github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/twofactor.go (about) 1 package auth 2 3 import ( 4 "net/http" 5 "net/url" 6 "strconv" 7 8 "github.com/cozy/cozy-stack/model/instance" 9 "github.com/cozy/cozy-stack/model/session" 10 "github.com/cozy/cozy-stack/pkg/config/config" 11 "github.com/cozy/cozy-stack/pkg/limits" 12 "github.com/cozy/cozy-stack/web/middlewares" 13 "github.com/labstack/echo/v4" 14 ) 15 16 func renderTwoFactorForm(c echo.Context, i *instance.Instance, code int, credsError string, twoFactorToken []byte) error { 17 title := i.Translate("Login Two factor title") 18 19 longRunSession, err := getTwoFactorLongRunSession(c) 20 if err != nil { 21 return err 22 } 23 redirect, err := getTwoFactorRedirect(c) 24 if err != nil { 25 return err 26 } 27 28 trustedDeviceCheckBox := true 29 trustedDeviceCheckBoxParam := c.QueryParam("trusted_device_checkbox") 30 if trustedDeviceCheckBoxParam != "" { 31 if b, err := strconv.ParseBool(trustedDeviceCheckBoxParam); err == nil { 32 trustedDeviceCheckBox = b 33 } 34 } 35 36 oauth := hasRedirectToAuthorize(i, redirect) 37 trustedCheckbox := !oauth && trustedDeviceCheckBox 38 39 return c.Render(code, "twofactor.html", echo.Map{ 40 "Domain": i.ContextualDomain(), 41 "ContextName": i.ContextName, 42 "Locale": i.Locale, 43 "Title": title, 44 "Favicon": middlewares.Favicon(i), 45 "CredentialsError": credsError, 46 "Redirect": redirect.String(), 47 "Confirm": c.FormValue("confirm"), 48 "State": c.FormValue("state"), 49 "ClientID": c.FormValue("client_id"), 50 "LongRunSession": longRunSession, 51 "TwoFactorToken": string(twoFactorToken), 52 "TrustedDeviceCheckBox": trustedCheckbox, 53 }) 54 } 55 56 func getTwoFactorLongRunSession(c echo.Context) (bool, error) { 57 if longRunParam := c.QueryParam("long-run-session"); longRunParam != "" { 58 longRun, err := strconv.ParseBool(longRunParam) 59 if err != nil { 60 return false, err 61 } 62 return longRun, nil 63 } 64 return false, nil 65 } 66 67 func getTwoFactorRedirect(c echo.Context) (*url.URL, error) { 68 inst := middlewares.GetInstance(c) 69 if c.FormValue("client_id") != "" { 70 return url.Parse(c.FormValue("redirect")) 71 } 72 return checkRedirectParam(c, inst.DefaultRedirection()) 73 } 74 75 // twoFactorForm handles the twoFactor from GET request 76 func twoFactorForm(c echo.Context) error { 77 inst := middlewares.GetInstance(c) 78 twoFactorTokenParam := c.QueryParam("two_factor_token") 79 if twoFactorTokenParam == "" { 80 return c.JSON(http.StatusBadRequest, "Missing twoFactorToken") 81 } 82 twoFactorToken := []byte(twoFactorTokenParam) 83 84 return renderTwoFactorForm(c, inst, http.StatusOK, "", twoFactorToken) 85 } 86 87 // twoFactor handles a the twoFactor POST request 88 func twoFactor(c echo.Context) error { 89 inst := middlewares.GetInstance(c) 90 if !inst.HasAuthMode(instance.TwoFactorMail) { 91 errorMessage := inst.Translate(TwoFactorErrorKey) 92 return c.JSON(http.StatusUnauthorized, echo.Map{ 93 "error": errorMessage, 94 }) 95 } 96 97 // Retreiving data from request 98 token := []byte(c.FormValue("two-factor-token")) 99 passcode := c.FormValue("two-factor-passcode") 100 generateTrustedDeviceToken, _ := strconv.ParseBool(c.FormValue("two-factor-generate-trusted-device-token")) 101 102 // Handle 2FA failed 103 correctPasscode := inst.ValidateTwoFactorPasscode(token, passcode) 104 if !correctPasscode { 105 return twoFactorFailed(c, inst, token) 106 } 107 108 // Special case when the 2FA validation is for confirming authentication, 109 // not creating a new session. 110 if c.FormValue("confirm") == "true" { 111 return ConfirmSuccess(c, inst, c.FormValue("state")) 112 } 113 114 // Special case when the 2FA validation if for moving a Cozy to this 115 // instance. 116 if c.FormValue("client_id") != "" { 117 u, err := moveSuccessURI(c) 118 if err != nil { 119 return err 120 } 121 return c.JSON(http.StatusOK, echo.Map{ 122 "redirect": u, 123 }) 124 } 125 126 // Generate a new session 127 longRunSession, err := getTwoFactorLongRunSession(c) 128 if err != nil { 129 return err 130 } 131 redirect, err := getTwoFactorRedirect(c) 132 if err != nil { 133 return err 134 } 135 duration := session.NormalRun 136 if longRunSession { 137 duration = session.LongRun 138 } else if hasRedirectToAuthorize(inst, redirect) { 139 duration = session.ShortRun 140 } 141 if err := newSession(c, inst, redirect, duration, "2FA"); err != nil { 142 return err 143 } 144 145 // Check if the user trusts its device 146 var generatedTrustedDeviceToken []byte 147 if generateTrustedDeviceToken { 148 generatedTrustedDeviceToken, _ = inst.GenerateTwoFactorTrustedDeviceSecret(c.Request()) 149 } 150 if wantsJSON(c) { 151 result := echo.Map{"redirect": redirect.String()} 152 if len(generatedTrustedDeviceToken) > 0 { 153 result["two_factor_trusted_device_token"] = string(generatedTrustedDeviceToken) 154 } 155 return c.JSON(http.StatusOK, result) 156 } 157 158 return c.Redirect(http.StatusSeeOther, redirect.String()) 159 } 160 161 // twoFactorFailed returns the 2FA form with an error message 162 func twoFactorFailed(c echo.Context, inst *instance.Instance, token []byte) error { 163 errorMessage := inst.Translate(TwoFactorErrorKey) 164 errCheckRateLimit := config.GetRateLimiter().CheckRateLimit(inst, limits.TwoFactorType) 165 if errCheckRateLimit == limits.ErrRateLimitExceeded { 166 if err := TwoFactorRateExceeded(inst); err != nil { 167 inst.Logger().WithNamespace("auth").Warn(err.Error()) 168 errorMessage = inst.Translate(TwoFactorExceededErrorKey) 169 } 170 } 171 172 // Render either the passcode page or a JSON message 173 if wantsJSON(c) { 174 return c.JSON(http.StatusUnauthorized, echo.Map{ 175 "error": errorMessage, 176 }) 177 } 178 return renderTwoFactorForm(c, inst, http.StatusUnauthorized, errorMessage, token) 179 }