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