forked from hailey.at/cocoon
An atproto PDS written in Go

implement providing 2FA token on PDS account login screen

Signed-off-by: Will Andrews <did:plc:dadhhalkfcq3gucaq25hjqon>

+56 -9
server/handle_account_signin.go
··· 2 3 import ( 4 "errors" 5 "strings" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "github.com/gorilla/sessions" ··· 15 ) 16 17 type OauthSigninInput struct { 18 - Username string `form:"username"` 19 - Password string `form:"password"` 20 - QueryParams string `form:"query_params"` 21 } 22 23 func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) { ··· 44 func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any { 45 defer sess.Save(e.Request(), e.Response()) 46 return map[string]any{ 47 - "errors": sess.Flashes("error"), 48 - "successes": sess.Flashes("success"), 49 } 50 } 51 ··· 83 idtype = "email" 84 } 85 86 // TODO: we should make this a helper since we do it for the base create_session as well 87 var repo models.RepoActor 88 var err error ··· 101 sess.AddFlash("Something went wrong!", "error") 102 } 103 sess.Save(e.Request(), e.Response()) 104 - return e.Redirect(303, "/account/signin") 105 } 106 107 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { ··· 111 sess.AddFlash("Something went wrong!", "error") 112 } 113 sess.Save(e.Request(), e.Response()) 114 - return e.Redirect(303, "/account/signin") 115 } 116 117 sess.Options = &sessions.Options{ ··· 127 return err 128 } 129 130 - if req.QueryParams != "" { 131 - return e.Redirect(303, "/oauth/authorize?"+req.QueryParams) 132 } else { 133 return e.Redirect(303, "/account") 134 }
··· 2 3 import ( 4 "errors" 5 + "fmt" 6 "strings" 7 + "time" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/gorilla/sessions" ··· 17 ) 18 19 type OauthSigninInput struct { 20 + Username string `form:"username"` 21 + Password string `form:"password"` 22 + AuthFactorToken string `form:"token"` 23 + QueryParams string `form:"query_params"` 24 } 25 26 func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) { ··· 47 func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any { 48 defer sess.Save(e.Request(), e.Response()) 49 return map[string]any{ 50 + "errors": sess.Flashes("error"), 51 + "successes": sess.Flashes("success"), 52 + "tokenrequired": sess.Flashes("tokenrequired"), 53 } 54 } 55 ··· 87 idtype = "email" 88 } 89 90 + queryParams := "" 91 + if req.QueryParams != "" { 92 + queryParams = fmt.Sprintf("?%s", req.QueryParams) 93 + } 94 + 95 // TODO: we should make this a helper since we do it for the base create_session as well 96 var repo models.RepoActor 97 var err error ··· 110 sess.AddFlash("Something went wrong!", "error") 111 } 112 sess.Save(e.Request(), e.Response()) 113 + return e.Redirect(303, "/account/signin"+queryParams) 114 } 115 116 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { ··· 120 sess.AddFlash("Something went wrong!", "error") 121 } 122 sess.Save(e.Request(), e.Response()) 123 + return e.Redirect(303, "/account/signin"+queryParams) 124 + } 125 + 126 + // if repo requires auth factor token and one hasn't been provided, return error prompting for one 127 + if repo.EmailAuthFactor && req.AuthFactorToken == "" { 128 + err = s.createAndSendAuthCode(ctx, repo) 129 + if err != nil { 130 + sess.AddFlash("Something went wrong!", "error") 131 + sess.Save(e.Request(), e.Response()) 132 + return e.Redirect(303, "/account/signin"+queryParams) 133 + } 134 + 135 + sess.AddFlash("requires 2FA token", "tokenrequired") 136 + sess.Save(e.Request(), e.Response()) 137 + return e.Redirect(303, "/account/signin"+queryParams) 138 + } 139 + 140 + // if auth factor is required, now check that the one provided is valid 141 + if repo.EmailAuthFactor { 142 + if repo.AuthCode == nil || repo.AuthCodeExpiresAt == nil { 143 + err = s.createAndSendAuthCode(ctx, repo) 144 + if err != nil { 145 + sess.AddFlash("Something went wrong!", "error") 146 + sess.Save(e.Request(), e.Response()) 147 + return e.Redirect(303, "/account/signin"+queryParams) 148 + } 149 + 150 + sess.AddFlash("requires 2FA token", "tokenrequired") 151 + sess.Save(e.Request(), e.Response()) 152 + return e.Redirect(303, "/account/signin"+queryParams) 153 + } 154 + 155 + if *repo.AuthCode != req.AuthFactorToken { 156 + return helpers.InvalidTokenError(e) 157 + } 158 + 159 + if time.Now().UTC().After(*repo.AuthCodeExpiresAt) { 160 + return helpers.ExpiredTokenError(e) 161 + } 162 } 163 164 sess.Options = &sessions.Options{ ··· 174 return err 175 } 176 177 + if queryParams != "" { 178 + return e.Redirect(303, "/oauth/authorize"+queryParams) 179 } else { 180 return e.Redirect(303, "/account") 181 }
+8 -8
server/handle_server_create_session.go
··· 87 return helpers.ServerError(e, nil) 88 } 89 90 // if repo requires auth factor token and one hasn't been provided, return error prompting for one 91 if repo.EmailAuthFactor && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 92 err = s.createAndSendAuthCode(ctx, repo) ··· 103 if repo.AuthCode == nil || repo.AuthCodeExpiresAt == nil { 104 err = s.createAndSendAuthCode(ctx, repo) 105 if err != nil { 106 - s.logger.Error("sending auth code", "error", err) 107 return helpers.ServerError(e, nil) 108 } 109 ··· 117 if time.Now().UTC().After(*repo.AuthCodeExpiresAt) { 118 return helpers.ExpiredTokenError(e) 119 } 120 - } 121 - 122 - if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 123 - if err != bcrypt.ErrMismatchedHashAndPassword { 124 - logger.Error("erorr comparing hash and password", "error", err) 125 - } 126 - return helpers.InputError(e, to.StringPtr("InvalidRequest")) 127 } 128 129 sess, err := s.createSession(ctx, &repo.Repo)
··· 87 return helpers.ServerError(e, nil) 88 } 89 90 + if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 91 + if err != bcrypt.ErrMismatchedHashAndPassword { 92 + logger.Error("erorr comparing hash and password", "error", err) 93 + } 94 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 95 + } 96 + 97 // if repo requires auth factor token and one hasn't been provided, return error prompting for one 98 if repo.EmailAuthFactor && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 99 err = s.createAndSendAuthCode(ctx, repo) ··· 110 if repo.AuthCode == nil || repo.AuthCodeExpiresAt == nil { 111 err = s.createAndSendAuthCode(ctx, repo) 112 if err != nil { 113 + logger.Error("sending auth code", "error", err) 114 return helpers.ServerError(e, nil) 115 } 116 ··· 124 if time.Now().UTC().After(*repo.AuthCodeExpiresAt) { 125 return helpers.ExpiredTokenError(e) 126 } 127 } 128 129 sess, err := s.createSession(ctx, &repo.Repo)
+4
server/templates/signin.html
··· 26 type="password" 27 placeholder="Password" 28 /> 29 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 30 <button class="primary" type="submit" value="Login">Login</button> 31 </form>
··· 26 type="password" 27 placeholder="Password" 28 /> 29 + {{ if .flashes.tokenrequired }} 30 + <br /> 31 + <input name="token" id="token" placeholder="Enter your 2FA token" /> 32 + {{ end }} 33 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 34 <button class="primary" type="submit" value="Login">Login</button> 35 </form>