github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/flagship.go (about)

     1  package auth
     2  
     3  import (
     4  	"crypto/subtle"
     5  	"encoding/json"
     6  	"net/http"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/model/instance"
    11  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    12  	"github.com/cozy/cozy-stack/model/oauth"
    13  	"github.com/cozy/cozy-stack/model/session"
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/consts"
    16  	"github.com/cozy/cozy-stack/pkg/couchdb"
    17  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    18  	"github.com/cozy/cozy-stack/pkg/limits"
    19  	"github.com/cozy/cozy-stack/web/middlewares"
    20  	"github.com/labstack/echo/v4"
    21  )
    22  
    23  // CreateSessionCode is the handler for creating a session code by the flagship
    24  // app.
    25  func CreateSessionCode(c echo.Context) error {
    26  	inst := middlewares.GetInstance(c)
    27  	switch canCreateSessionCode(c, inst) {
    28  	case allowedToCreateSessionCode:
    29  		// OK
    30  	case need2FAToCreateSessionCode:
    31  		twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
    32  		if err != nil {
    33  			return err
    34  		}
    35  		return c.JSON(http.StatusForbidden, echo.Map{
    36  			"error":            "two factor needed",
    37  			"two_factor_token": string(twoFactorToken),
    38  		})
    39  	default:
    40  		return c.JSON(http.StatusUnauthorized, echo.Map{
    41  			"error": "Not authorized",
    42  		})
    43  	}
    44  
    45  	return ReturnSessionCode(c, http.StatusCreated, inst)
    46  }
    47  
    48  func ReturnSessionCode(c echo.Context, statusCode int, inst *instance.Instance) error {
    49  	code, err := inst.CreateSessionCode()
    50  	if err != nil {
    51  		return c.JSON(http.StatusInternalServerError, echo.Map{
    52  			"error": err,
    53  		})
    54  	}
    55  
    56  	req := c.Request()
    57  	var ip string
    58  	if forwardedFor := req.Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" {
    59  		ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0])
    60  	}
    61  	if ip == "" {
    62  		ip = strings.Split(req.RemoteAddr, ":")[0]
    63  	}
    64  	inst.Logger().WithField("nspace", "loginaudit").
    65  		Infof("New session_code created from %s at %s", ip, time.Now())
    66  
    67  	return c.JSON(statusCode, echo.Map{
    68  		"session_code": code,
    69  	})
    70  }
    71  
    72  type sessionCodeParameters struct {
    73  	Passphrase     string `json:"passphrase"`
    74  	TwoFactorToken string `json:"two_factor_token"`
    75  	TwoFactorCode  string `json:"two_factor_passcode"`
    76  }
    77  
    78  type canCreateSessionCodeResult int
    79  
    80  const (
    81  	allowedToCreateSessionCode canCreateSessionCodeResult = iota
    82  	cannotCreateSessionCode
    83  	need2FAToCreateSessionCode
    84  )
    85  
    86  func canCreateSessionCode(c echo.Context, inst *instance.Instance) canCreateSessionCodeResult {
    87  	if err := middlewares.AllowMaximal(c); err == nil {
    88  		return allowedToCreateSessionCode
    89  	}
    90  
    91  	var args sessionCodeParameters
    92  	if err := c.Bind(&args); err != nil {
    93  		return cannotCreateSessionCode
    94  	}
    95  	if err := instance.CheckPassphrase(inst, []byte(args.Passphrase)); err != nil {
    96  		return cannotCreateSessionCode
    97  	}
    98  
    99  	if inst.HasAuthMode(instance.TwoFactorMail) {
   100  		token := []byte(args.TwoFactorToken)
   101  		if ok := inst.ValidateTwoFactorPasscode(token, args.TwoFactorCode); !ok {
   102  			return need2FAToCreateSessionCode
   103  		}
   104  	}
   105  	return allowedToCreateSessionCode
   106  }
   107  
   108  func postChallenge(c echo.Context) error {
   109  	inst := middlewares.GetInstance(c)
   110  	err := config.GetRateLimiter().CheckRateLimit(inst, limits.OAuthClientType)
   111  	if limits.IsLimitReachedOrExceeded(err) {
   112  		return echo.NewHTTPError(http.StatusNotFound, "Not found")
   113  	}
   114  	client := c.Get("client").(*oauth.Client)
   115  	nonce, err := client.CreateChallenge(inst)
   116  	if err != nil {
   117  		return c.JSON(http.StatusInternalServerError, echo.Map{"error": err.Error()})
   118  	}
   119  	return c.JSON(http.StatusCreated, echo.Map{"nonce": nonce})
   120  }
   121  
   122  func postAttestation(c echo.Context) error {
   123  	inst := middlewares.GetInstance(c)
   124  	client, err := oauth.FindClient(inst, c.Param("client-id"))
   125  	if err != nil {
   126  		return c.JSON(http.StatusNotFound, echo.Map{
   127  			"error": "Client not found",
   128  		})
   129  	}
   130  	var data oauth.AttestationRequest
   131  	if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil {
   132  		return c.JSON(http.StatusBadRequest, echo.Map{
   133  			"error": err.Error(),
   134  		})
   135  	}
   136  	if err := client.Attest(inst, data); err != nil {
   137  		inst.Logger().Infof("Cannot attest %s client: %s", client.ID(), err.Error())
   138  		return c.JSON(http.StatusBadRequest, echo.Map{
   139  			"error": err.Error(),
   140  		})
   141  	}
   142  	return c.NoContent(http.StatusNoContent)
   143  }
   144  
   145  func confirmFlagship(c echo.Context) error {
   146  	inst := middlewares.GetInstance(c)
   147  	client, err := oauth.FindClient(inst, c.Param("client-id"))
   148  	if err != nil {
   149  		return c.JSON(http.StatusNotFound, echo.Map{
   150  			"error": "Client not found",
   151  		})
   152  	}
   153  
   154  	err = config.GetRateLimiter().CheckRateLimit(inst, limits.ConfirmFlagshipType)
   155  	if limits.IsLimitReachedOrExceeded(err) {
   156  		return c.JSON(http.StatusUnauthorized, echo.Map{
   157  			"error": inst.Translate("Confirm Flagship Invalid code"),
   158  		})
   159  	}
   160  
   161  	clientID := c.Param("client-id")
   162  	code := c.FormValue("code")
   163  	token := []byte(c.FormValue("token"))
   164  	if ok := oauth.CheckFlagshipCode(inst, clientID, token, code); !ok {
   165  		return c.JSON(http.StatusUnauthorized, echo.Map{
   166  			"error": inst.Translate("Confirm Flagship Invalid code"),
   167  		})
   168  	}
   169  
   170  	if err := client.SetFlagship(inst); err != nil {
   171  		return c.JSON(http.StatusInternalServerError, echo.Map{
   172  			"error": err.Error,
   173  		})
   174  	}
   175  	return c.NoContent(http.StatusNoContent)
   176  }
   177  
   178  type loginFlagshipParameters struct {
   179  	ClientID          string `json:"client_id"`
   180  	ClientSecret      string `json:"client_secret"`
   181  	Passphrase        string `json:"passphrase"`
   182  	TwoFactorPasscode string `json:"two_factor_passcode"`
   183  	TwoFactorToken    string `json:"two_factor_token"`
   184  	EmailVerifiedCode string `json:"email_verified_code"`
   185  }
   186  
   187  func loginFlagship(c echo.Context) error {
   188  	inst := middlewares.GetInstance(c)
   189  
   190  	var args loginFlagshipParameters
   191  	if err := c.Bind(&args); err != nil {
   192  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
   193  	}
   194  
   195  	if instance.CheckPassphrase(inst, []byte(args.Passphrase)) != nil {
   196  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
   197  		if limits.IsLimitReachedOrExceeded(err) {
   198  			if err = LoginRateExceeded(inst); err != nil {
   199  				inst.Logger().WithNamespace("auth").Warn(err.Error())
   200  			}
   201  		}
   202  		return c.JSON(http.StatusUnauthorized, echo.Map{
   203  			"error": inst.Translate(CredentialsErrorKey),
   204  		})
   205  	}
   206  
   207  	if inst.HasAuthMode(instance.TwoFactorMail) && !inst.CheckEmailVerifiedCode(args.EmailVerifiedCode) {
   208  		if len(args.TwoFactorToken) == 0 {
   209  			twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
   210  			if err != nil {
   211  				return err
   212  			}
   213  			return c.JSON(http.StatusUnauthorized, echo.Map{
   214  				"two_factor_token": string(twoFactorToken),
   215  			})
   216  		}
   217  		twoFactorToken := []byte(args.TwoFactorToken)
   218  		if !inst.ValidateTwoFactorPasscode(twoFactorToken, args.TwoFactorPasscode) {
   219  			return c.JSON(http.StatusForbidden, echo.Map{
   220  				"error": inst.Translate(TwoFactorErrorKey),
   221  			})
   222  		}
   223  	}
   224  
   225  	client, err := oauth.FindClient(inst, args.ClientID)
   226  	if err != nil {
   227  		if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 {
   228  			return err
   229  		}
   230  		return c.JSON(http.StatusBadRequest, echo.Map{
   231  			"error": "the client must be registered",
   232  		})
   233  	}
   234  	if subtle.ConstantTimeCompare([]byte(args.ClientSecret), []byte(client.ClientSecret)) == 0 {
   235  		return c.JSON(http.StatusBadRequest, echo.Map{
   236  			"error": "invalid client_secret",
   237  		})
   238  	}
   239  
   240  	if !client.Flagship {
   241  		return ReturnSessionCode(c, http.StatusAccepted, inst)
   242  	}
   243  
   244  	if client.Pending {
   245  		client.Pending = false
   246  		client.ClientID = ""
   247  		_ = couchdb.UpdateDoc(inst, client)
   248  		client.ClientID = client.CouchID
   249  	}
   250  
   251  	if err := session.SendNewRegistrationNotification(inst, client.ClientID); err != nil {
   252  		return c.JSON(http.StatusInternalServerError, echo.Map{
   253  			"error": err.Error(),
   254  		})
   255  	}
   256  
   257  	out := AccessTokenReponse{
   258  		Type:  "bearer",
   259  		Scope: "*",
   260  	}
   261  	out.Refresh, err = client.CreateJWT(inst, consts.RefreshTokenAudience, out.Scope)
   262  	if err != nil {
   263  		return c.JSON(http.StatusInternalServerError, echo.Map{
   264  			"error": "Can't generate refresh token",
   265  		})
   266  	}
   267  	out.Access, err = client.CreateJWT(inst, consts.AccessTokenAudience, out.Scope)
   268  	if err != nil {
   269  		return c.JSON(http.StatusInternalServerError, echo.Map{
   270  			"error": "Can't generate access token",
   271  		})
   272  	}
   273  	return c.JSON(http.StatusOK, out)
   274  }