github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/passphrase.go (about) 1 package auth 2 3 import ( 4 "encoding/hex" 5 "errors" 6 "net/http" 7 "net/url" 8 "strconv" 9 10 "github.com/cozy/cozy-stack/model/bitwarden" 11 "github.com/cozy/cozy-stack/model/bitwarden/settings" 12 "github.com/cozy/cozy-stack/model/instance" 13 "github.com/cozy/cozy-stack/model/instance/lifecycle" 14 "github.com/cozy/cozy-stack/model/sharing" 15 "github.com/cozy/cozy-stack/pkg/config/config" 16 "github.com/cozy/cozy-stack/pkg/consts" 17 "github.com/cozy/cozy-stack/pkg/couchdb" 18 "github.com/cozy/cozy-stack/pkg/crypto" 19 "github.com/cozy/cozy-stack/pkg/limits" 20 "github.com/cozy/cozy-stack/web/middlewares" 21 "github.com/labstack/echo/v4" 22 ) 23 24 func passphraseResetForm(c echo.Context) error { 25 instance := middlewares.GetInstance(c) 26 if !instance.OnboardingFinished { 27 return middlewares.RenderNeedOnboarding(c, instance) 28 } 29 30 hasHint := false 31 if setting, err := settings.Get(instance); err == nil { 32 hasHint = setting.PassphraseHint != "" 33 } 34 hasCiphers := true 35 if resp, err := couchdb.NormalDocs(instance, consts.BitwardenCiphers, 0, 1, "", false); err == nil { 36 hasCiphers = resp.Total > 0 37 } 38 backButton := "" 39 from := c.QueryParam("from") 40 if from != "" { 41 backButton = instance.SubDomain(from).String() 42 } else if c.QueryParam("hideBackButton") != "true" { 43 backButton = instance.PageURL("/auth/login", nil) 44 } 45 forcedOIDC := instance.HasForcedOIDC() 46 return c.Render(http.StatusOK, "passphrase_reset.html", echo.Map{ 47 "Domain": instance.ContextualDomain(), 48 "ContextName": instance.ContextName, 49 "Locale": instance.Locale, 50 "Title": instance.TemplateTitle(), 51 "Favicon": middlewares.Favicon(instance), 52 "CSRF": c.Get("csrf"), 53 "Redirect": c.QueryParam("redirect"), 54 "HasHint": hasHint, 55 "HasCiphers": hasCiphers, 56 "CozyPass": forcedOIDC, 57 "From": from, 58 "BackButton": backButton, 59 }) 60 } 61 62 func passphraseForm(c echo.Context) error { 63 inst := middlewares.GetInstance(c) 64 registerToken := c.QueryParams().Get("registerToken") 65 if inst.OnboardingFinished { 66 redirect := inst.DefaultRedirection() 67 return c.Redirect(http.StatusSeeOther, redirect.String()) 68 } 69 70 if registerToken == "" || !middlewares.CheckRegisterToken(c, inst) { 71 return middlewares.RenderNeedOnboarding(c, inst) 72 } 73 74 cryptoPolyfill := middlewares.CryptoPolyfill(c) 75 iterations := crypto.DefaultPBKDF2Iterations 76 if cryptoPolyfill { 77 iterations = crypto.MinPBKDF2Iterations 78 } 79 80 return c.Render(http.StatusOK, "passphrase_choose.html", echo.Map{ 81 "Domain": inst.ContextualDomain(), 82 "ContextName": inst.ContextName, 83 "Locale": inst.Locale, 84 "Title": inst.TemplateTitle(), 85 "Favicon": middlewares.Favicon(inst), 86 "Action": "/settings/passphrase", 87 "Iterations": iterations, 88 "Salt": string(inst.PassphraseSalt()), 89 "RegisterToken": registerToken, 90 "CryptoPolyfill": cryptoPolyfill, 91 }) 92 } 93 94 func sendHint(c echo.Context) error { 95 i := middlewares.GetInstance(c) 96 if err := config.GetRateLimiter().CheckRateLimit(i, limits.SendHintByMail); err == nil { 97 if err := lifecycle.SendHint(i); err != nil { 98 return err 99 } 100 } 101 var u url.Values 102 if redirect := c.FormValue("redirect"); redirect != "" { 103 u = url.Values{"redirect": {redirect}} 104 } 105 return c.Render(http.StatusOK, "error.html", echo.Map{ 106 "Domain": i.ContextualDomain(), 107 "ContextName": i.ContextName, 108 "Locale": i.Locale, 109 "Title": i.TemplateTitle(), 110 "Favicon": middlewares.Favicon(i), 111 "Inverted": true, 112 "Illustration": "/images/mail-sent.svg", 113 "ErrorTitle": "Hint sent Title", 114 "Error": "Hint sent Body", 115 "ErrorDetail": "Hint sent Detail", 116 "SupportEmail": i.SupportEmailAddress(), 117 "Button": "Hint sent Login Button", 118 "ButtonURL": i.PageURL("/auth/login", u), 119 }) 120 } 121 122 func passphraseReset(c echo.Context) error { 123 i := middlewares.GetInstance(c) 124 from := c.FormValue("from") 125 if err := lifecycle.RequestPassphraseReset(i, from); err != nil && !errors.Is(err, instance.ErrResetAlreadyRequested) { 126 return err 127 } 128 // Disconnect the user if it is logged in. The idea is that if the user 129 // (maybe by accident) asks for a passphrase reset while logged in, we log 130 // him out to be able to re-go through the process of logging back-in. It is 131 // more a UX choice than a "security" one. 132 session, ok := middlewares.GetSession(c) 133 if ok { 134 c.SetCookie(session.Delete(i)) 135 } 136 var u url.Values 137 if redirect := c.FormValue("redirect"); redirect != "" { 138 u = url.Values{"redirect": {redirect}} 139 } 140 return c.Render(http.StatusOK, "error.html", echo.Map{ 141 "Domain": i.ContextualDomain(), 142 "ContextName": i.ContextName, 143 "Locale": i.Locale, 144 "Title": i.TemplateTitle(), 145 "Favicon": middlewares.Favicon(i), 146 "Inverted": true, 147 "Illustration": "/images/mail-sent.svg", 148 "ErrorTitle": "Passphrase is reset Title", 149 "Error": "Passphrase is reset Body", 150 "ErrorDetail": "Passphrase is reset Detail", 151 "SupportEmail": i.SupportEmailAddress(), 152 "Button": "Passphrase is reset Login Button", 153 "ButtonURL": i.PageURL("/auth/login", u), 154 }) 155 } 156 157 func passphraseRenewForm(c echo.Context) error { 158 inst := middlewares.GetInstance(c) 159 if middlewares.IsLoggedIn(c) { 160 redirect := inst.DefaultRedirection().String() 161 return c.Redirect(http.StatusSeeOther, redirect) 162 } 163 164 // Check that the token is actually defined and well encoded. The actual 165 // token value checking is also done on the passphraseRenew handler. 166 token, err := hex.DecodeString(c.QueryParam("token")) 167 if err != nil || len(token) == 0 { 168 return renderError(c, http.StatusBadRequest, "Error Invalid reset token") 169 } 170 if err = lifecycle.CheckPassphraseRenewToken(inst, token); err != nil { 171 if errors.Is(err, instance.ErrMissingToken) { 172 return renderError(c, http.StatusBadRequest, "Error Invalid reset token") 173 } 174 return c.JSON(http.StatusBadRequest, echo.Map{ 175 "error": "invalid_token", 176 }) 177 } 178 179 cryptoPolyfill := middlewares.CryptoPolyfill(c) 180 iterations := crypto.DefaultPBKDF2Iterations 181 if cryptoPolyfill { 182 iterations = crypto.MinPBKDF2Iterations 183 } 184 185 return c.Render(http.StatusOK, "passphrase_choose.html", echo.Map{ 186 "Domain": inst.ContextualDomain(), 187 "ContextName": inst.ContextName, 188 "Locale": inst.Locale, 189 "Title": inst.TemplateTitle(), 190 "Favicon": middlewares.Favicon(inst), 191 "Action": "/auth/passphrase_renew", 192 "From": c.QueryParam("from"), 193 "Iterations": iterations, 194 "Salt": string(inst.PassphraseSalt()), 195 "ResetToken": hex.EncodeToString(token), 196 "CSRF": c.Get("csrf"), 197 "CryptoPolyfill": cryptoPolyfill, 198 }) 199 } 200 201 func passphraseRenew(c echo.Context) error { 202 inst := middlewares.GetInstance(c) 203 if middlewares.IsLoggedIn(c) { 204 redirect := inst.DefaultRedirection().String() 205 if wantsJSON(c) { 206 return c.JSON(http.StatusOK, echo.Map{"redirect": redirect}) 207 } 208 return c.Redirect(http.StatusSeeOther, redirect) 209 } 210 pass := []byte(c.FormValue("passphrase")) 211 iterations, _ := strconv.Atoi(c.FormValue("iterations")) 212 token, err := hex.DecodeString(c.FormValue("passphrase_reset_token")) 213 if err != nil { 214 if wantsJSON(c) { 215 return c.JSON(http.StatusUnauthorized, echo.Map{ 216 "error": "Invalid reset token", 217 }) 218 } 219 return renderError(c, http.StatusBadRequest, "Error Invalid reset token") 220 } 221 err = lifecycle.PassphraseRenew(inst, token, lifecycle.PassParameters{ 222 Pass: pass, 223 Iterations: iterations, 224 Key: c.FormValue("key"), 225 PublicKey: c.FormValue("public_key"), 226 PrivateKey: c.FormValue("private_key"), 227 Hint: c.FormValue("hint"), 228 }) 229 if err != nil { 230 if errors.Is(err, instance.ErrMissingToken) { 231 if wantsJSON(c) { 232 return c.JSON(http.StatusUnauthorized, echo.Map{ 233 "error": "Invalid reset token", 234 }) 235 } 236 return renderError(c, http.StatusBadRequest, "Error Invalid reset token") 237 } 238 return c.JSON(http.StatusBadRequest, echo.Map{ 239 "error": "invalid_token", 240 }) 241 } 242 // Before deleting the ciphers, it will revoke the sharings to avoid deleting 243 // the ciphers on the Cozy instances of the other members. 244 if err := sharing.RevokeCipherSharings(inst); err == nil { 245 if err := bitwarden.DeleteUnrecoverableCiphers(inst); err != nil { 246 inst.Logger().WithNamespace("bitwarden"). 247 Warnf("Error on ciphers deletion after password reset: %s", err) 248 } 249 } 250 251 redirect := inst.PageURL("/auth/login", nil) 252 if c.FormValue("from") == consts.SettingsSlug { 253 u := inst.SubDomain(consts.SettingsSlug) 254 u.Fragment = "/profile/email" 255 redirect = u.String() 256 } 257 if wantsJSON(c) { 258 return c.JSON(http.StatusOK, echo.Map{"redirect": redirect}) 259 } 260 return c.Redirect(http.StatusSeeOther, redirect) 261 }