github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/magic_link.go (about) 1 package auth 2 3 import ( 4 "crypto/subtle" 5 "net/http" 6 7 "github.com/cozy/cozy-stack/model/bitwarden/settings" 8 "github.com/cozy/cozy-stack/model/instance" 9 "github.com/cozy/cozy-stack/model/instance/lifecycle" 10 "github.com/cozy/cozy-stack/model/oauth" 11 "github.com/cozy/cozy-stack/model/session" 12 "github.com/cozy/cozy-stack/pkg/config/config" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/pkg/jsonapi" 16 "github.com/cozy/cozy-stack/pkg/limits" 17 "github.com/cozy/cozy-stack/web/middlewares" 18 "github.com/labstack/echo/v4" 19 ) 20 21 func sendMagicLink(c echo.Context) error { 22 inst := middlewares.GetInstance(c) 23 24 err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType) 25 if limits.IsLimitReachedOrExceeded(err) { 26 return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests") 27 } 28 29 redirect := c.FormValue("redirect") 30 if err := lifecycle.SendMagicLink(inst, redirect); err != nil { 31 return err 32 } 33 return c.Render(http.StatusOK, "error.html", echo.Map{ 34 "Domain": inst.ContextualDomain(), 35 "ContextName": inst.ContextName, 36 "Locale": inst.Locale, 37 "Title": inst.TemplateTitle(), 38 "Favicon": middlewares.Favicon(inst), 39 "Inverted": true, 40 "Illustration": "/images/mail-sent.svg", 41 "ErrorTitle": "Magic link has been sent Title", 42 "Error": "Magic link has been sent Body", 43 "ErrorDetail": "Magic link has been sent Detail", 44 "SupportEmail": inst.SupportEmailAddress(), 45 }) 46 } 47 48 func loginWithMagicLink(c echo.Context) error { 49 inst := middlewares.GetInstance(c) 50 redirect, err := checkRedirectParam(c, inst.DefaultRedirection()) 51 if err != nil { 52 return err 53 } 54 55 if _, ok := middlewares.GetSession(c); ok { 56 return c.Redirect(http.StatusSeeOther, redirect.String()) 57 } 58 59 code := c.QueryParam("code") // Login 60 if code == "" { 61 code = c.QueryParam("magic_code") // Onboarding from the cloudery 62 } 63 if err := lifecycle.CheckMagicLink(inst, code); err != nil { 64 err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType) 65 if limits.IsLimitReachedOrExceeded(err) { 66 return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests") 67 } 68 return renderError(c, http.StatusBadRequest, "Error Invalid magic link") 69 } 70 71 if inst.HasAuthMode(instance.TwoFactorMail) { 72 iterations := 0 73 if settings, err := settings.Get(inst); err == nil { 74 iterations = settings.PassphraseKdfIterations 75 } 76 return c.Render(http.StatusOK, "magic_link_twofactor.html", echo.Map{ 77 "TemplateTitle": inst.TemplateTitle(), 78 "Domain": inst.ContextualDomain(), 79 "ContextName": inst.ContextName, 80 "Locale": inst.Locale, 81 "Iterations": iterations, 82 "Salt": string(inst.PassphraseSalt()), 83 "CSRF": c.Get("csrf"), 84 "Favicon": middlewares.Favicon(inst), 85 "BottomNavBar": middlewares.BottomNavigationBar(c), 86 "CryptoPolyfill": middlewares.CryptoPolyfill(c), 87 "MagicCode": code, 88 "Redirect": redirect, 89 }) 90 } 91 92 err = newSession(c, inst, redirect, session.NormalRun, "magic_link") 93 if err != nil { 94 return err 95 } 96 return c.Redirect(http.StatusSeeOther, redirect.String()) 97 } 98 99 func loginWithMagicLinkAndPassword(c echo.Context) error { 100 inst := middlewares.GetInstance(c) 101 code := c.FormValue("magic_code") 102 103 // Check magic code 104 if err := lifecycle.CheckMagicLink(inst, code); err != nil { 105 err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType) 106 if limits.IsLimitReachedOrExceeded(err) { 107 return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests") 108 } 109 return c.JSON(http.StatusUnauthorized, echo.Map{ 110 "error": inst.Translate("Error Invalid magic link"), 111 }) 112 } 113 114 // Check passphrase 115 passphrase := []byte(c.FormValue("passphrase")) 116 if instance.CheckPassphrase(inst, passphrase) != nil { 117 errorMessage := inst.Translate(CredentialsErrorKey) 118 err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType) 119 if limits.IsLimitReachedOrExceeded(err) { 120 if err = LoginRateExceeded(inst); err != nil { 121 inst.Logger().WithNamespace("auth").Warn(err.Error()) 122 } 123 } 124 return c.JSON(http.StatusUnauthorized, echo.Map{ 125 "error": errorMessage, 126 }) 127 } 128 129 redirect, err := checkRedirectParam(c, inst.DefaultRedirection()) 130 if err != nil { 131 return err 132 } 133 err = newSession(c, inst, redirect, session.NormalRun, "magic_link") 134 if err != nil { 135 return err 136 } 137 return c.JSON(http.StatusOK, echo.Map{ 138 "redirect": redirect.String(), 139 }) 140 } 141 142 type magicLinkFlagshipParameters struct { 143 ClientID string `json:"client_id"` 144 ClientSecret string `json:"client_secret"` 145 Code string `json:"magic_code"` 146 Passphrase string `json:"passphrase"` 147 } 148 149 func magicLinkFlagship(c echo.Context) error { 150 inst := middlewares.GetInstance(c) 151 152 var args magicLinkFlagshipParameters 153 if err := c.Bind(&args); err != nil { 154 return jsonapi.Errorf(http.StatusBadRequest, "%s", err) 155 } 156 157 if err := lifecycle.CheckMagicLink(inst, args.Code); err != nil { 158 err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType) 159 if limits.IsLimitReachedOrExceeded(err) { 160 return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests") 161 } 162 return c.JSON(http.StatusUnauthorized, echo.Map{ 163 "error": "invalid magic code", 164 }) 165 } 166 167 if inst.HasAuthMode(instance.TwoFactorMail) { 168 if instance.CheckPassphrase(inst, []byte(args.Passphrase)) != nil { 169 err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType) 170 if limits.IsLimitReachedOrExceeded(err) { 171 if err = LoginRateExceeded(inst); err != nil { 172 inst.Logger().WithNamespace("auth").Warn(err.Error()) 173 } 174 } 175 return c.JSON(http.StatusUnauthorized, echo.Map{ 176 "error": "passphrase is required as second authentication factor", 177 }) 178 } 179 } 180 181 client, err := oauth.FindClient(inst, args.ClientID) 182 if err != nil { 183 if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 { 184 return err 185 } 186 return c.JSON(http.StatusBadRequest, echo.Map{ 187 "error": "the client must be registered", 188 }) 189 } 190 if subtle.ConstantTimeCompare([]byte(args.ClientSecret), []byte(client.ClientSecret)) == 0 { 191 return c.JSON(http.StatusBadRequest, echo.Map{ 192 "error": "invalid client_secret", 193 }) 194 } 195 196 if !client.Flagship { 197 return ReturnSessionCode(c, http.StatusAccepted, inst) 198 } 199 200 if client.Pending { 201 client.Pending = false 202 client.ClientID = "" 203 _ = couchdb.UpdateDoc(inst, client) 204 client.ClientID = client.CouchID 205 } 206 207 if err := session.SendNewRegistrationNotification(inst, client.ClientID); err != nil { 208 return c.JSON(http.StatusInternalServerError, echo.Map{ 209 "error": err.Error(), 210 }) 211 } 212 213 out := AccessTokenReponse{ 214 Type: "bearer", 215 Scope: "*", 216 } 217 out.Refresh, err = client.CreateJWT(inst, consts.RefreshTokenAudience, out.Scope) 218 if err != nil { 219 return c.JSON(http.StatusInternalServerError, echo.Map{ 220 "error": "Can't generate refresh token", 221 }) 222 } 223 out.Access, err = client.CreateJWT(inst, consts.AccessTokenAudience, out.Scope) 224 if err != nil { 225 return c.JSON(http.StatusInternalServerError, echo.Map{ 226 "error": "Can't generate access token", 227 }) 228 } 229 return c.JSON(http.StatusOK, out) 230 }