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 }