github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/confirm.go (about) 1 package auth 2 3 import ( 4 "errors" 5 "net/http" 6 "net/url" 7 8 "github.com/cozy/cozy-stack/model/bitwarden/settings" 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/instance/lifecycle" 11 "github.com/cozy/cozy-stack/pkg/config/config" 12 "github.com/cozy/cozy-stack/pkg/consts" 13 "github.com/cozy/cozy-stack/pkg/couchdb" 14 "github.com/cozy/cozy-stack/pkg/limits" 15 "github.com/cozy/cozy-stack/pkg/realtime" 16 "github.com/cozy/cozy-stack/web/middlewares" 17 "github.com/labstack/echo/v4" 18 ) 19 20 func confirmForm(c echo.Context) error { 21 inst := middlewares.GetInstance(c) 22 redirect := c.QueryParam("redirect") 23 state := c.QueryParam("state") 24 if state == "" { 25 return renderError(c, http.StatusBadRequest, "Error No state parameter") 26 } 27 if inst.HasForcedOIDC() { 28 q := url.Values{"redirect": {redirect}, "confirm_state": {state}} 29 return c.Redirect(http.StatusSeeOther, inst.PageURL("/oidc/start", q)) 30 } 31 32 iterations := 0 33 if settings, err := settings.Get(inst); err == nil { 34 iterations = settings.PassphraseKdfIterations 35 } 36 return c.Render(http.StatusOK, "confirm_auth.html", echo.Map{ 37 "TemplateTitle": inst.TemplateTitle(), 38 "Domain": inst.ContextualDomain(), 39 "ContextName": inst.ContextName, 40 "Locale": inst.Locale, 41 "Iterations": iterations, 42 "Salt": string(inst.PassphraseSalt()), 43 "CSRF": c.Get("csrf"), 44 "Favicon": middlewares.Favicon(inst), 45 "BottomNavBar": middlewares.BottomNavigationBar(c), 46 "CryptoPolyfill": middlewares.CryptoPolyfill(c), 47 "State": state, 48 "Redirect": redirect, 49 }) 50 } 51 52 func confirmAuth(c echo.Context) error { 53 inst := middlewares.GetInstance(c) 54 if inst.HasForcedOIDC() { 55 return c.NoContent(http.StatusBadRequest) 56 } 57 state := c.FormValue("state") 58 59 // Check passphrase 60 passphrase := []byte(c.FormValue("passphrase")) 61 if instance.CheckPassphrase(inst, passphrase) != nil { 62 errorMessage := inst.Translate(CredentialsErrorKey) 63 err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType) 64 if limits.IsLimitReachedOrExceeded(err) { 65 if err = LoginRateExceeded(inst); err != nil { 66 inst.Logger().WithNamespace("auth").Warn(err.Error()) 67 } 68 } 69 return c.JSON(http.StatusUnauthorized, echo.Map{ 70 "error": errorMessage, 71 }) 72 } 73 74 if inst.HasAuthMode(instance.TwoFactorMail) && !isTrustedDevice(c, inst) { 75 twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst) 76 if err != nil { 77 return err 78 } 79 v := url.Values{} 80 v.Add("two_factor_token", string(twoFactorToken)) 81 v.Add("state", state) 82 v.Add("redirect", c.FormValue("redirect")) 83 v.Add("confirm", "true") 84 v.Add("trusted_device_checkbox", "false") 85 86 return c.JSON(http.StatusOK, echo.Map{ 87 "redirect": inst.PageURL("/auth/twofactor", v), 88 }) 89 } 90 91 return ConfirmSuccess(c, inst, state) 92 } 93 94 // ConfirmSuccess can be used to send a response after a successful identity 95 // confirmation. 96 func ConfirmSuccess(c echo.Context, inst *instance.Instance, state string) error { 97 doc := couchdb.JSONDoc{ 98 Type: consts.AuthConfirmations, 99 M: map[string]interface{}{ 100 "_id": state, 101 }, 102 } 103 realtime.GetHub().Publish(inst, realtime.EventCreate, &doc, nil) 104 105 redirect, err := checkRedirectToAuthorized(c) 106 if err != nil { 107 redirect, err = checkRedirectParam(c, inst.DefaultRedirection()) 108 } 109 if err != nil { 110 return err 111 } 112 code, err := GetStore().AddCode(inst) 113 if err != nil { 114 inst.Logger().Warnf("Cannot add confirm code: %s", err) 115 return c.NoContent(http.StatusInternalServerError) 116 } 117 q := redirect.Query() 118 q.Set("code", code) 119 q.Set("state", state) 120 redirect.RawQuery = q.Encode() 121 122 if wantsJSON(c) { 123 return c.JSON(http.StatusOK, echo.Map{ 124 "redirect": redirect.String(), 125 }) 126 } 127 return c.Redirect(http.StatusSeeOther, redirect.String()) 128 } 129 130 func checkRedirectToAuthorized(c echo.Context) (*url.URL, error) { 131 inst := middlewares.GetInstance(c) 132 redirect := c.FormValue("redirect") 133 u, err := url.Parse(redirect) 134 if err != nil { 135 return nil, echo.NewHTTPError(http.StatusBadRequest, "bad url: could not parse") 136 } 137 138 if ok := checkRedirectToManager(inst, u); ok { 139 return u, nil 140 } 141 142 for _, host := range config.GetConfig().AuthorizedForConfirm { 143 if host == u.Host { 144 return u, nil 145 } 146 } 147 148 return nil, errors.New("not authorized") 149 } 150 151 func checkRedirectToManager(inst *instance.Instance, redirect *url.URL) bool { 152 config, ok := inst.SettingsContext() 153 if !ok { 154 return false 155 } 156 managerURL, ok := config["manager_url"].(string) 157 if !ok { 158 return false 159 } 160 manager, err := url.Parse(managerURL) 161 if err != nil { 162 return false 163 } 164 return redirect.Scheme == manager.Scheme && redirect.Host == manager.Host 165 } 166 167 func confirmCode(c echo.Context) error { 168 inst := middlewares.GetInstance(c) 169 code := c.Param("code") 170 if code == "" { 171 return c.JSON(http.StatusUnauthorized, echo.Map{ 172 "error": "no code", 173 }) 174 } 175 ok, err := GetStore().GetCode(inst, code) 176 if err != nil { 177 inst.Logger().Warnf("Cannot get confirm code: %s", err) 178 return c.NoContent(http.StatusInternalServerError) 179 } 180 if !ok { 181 return c.JSON(http.StatusUnauthorized, echo.Map{ 182 "error": "invalid code", 183 }) 184 } 185 186 return c.NoContent(http.StatusNoContent) 187 }