A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

implement providing 2FA token on PDS account login screen

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

willdot.net (Jan 3, 2026, 6:57 AM UTC) 9b2309fd 86972c4f

+68 -17
+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 ··· 83 87 idtype = "email" 84 88 } 85 89 90 + queryParams := "" 91 + if req.QueryParams != "" { 92 + queryParams = fmt.Sprintf("?%s", req.QueryParams) 93 + } 94 + 86 95 // TODO: we should make this a helper since we do it for the base create_session as well 87 96 var repo models.RepoActor 88 97 var err error ··· 101 110 sess.AddFlash("Something went wrong!", "error") 102 111 } 103 112 sess.Save(e.Request(), e.Response()) 104 - return e.Redirect(303, "/account/signin") 113 + return e.Redirect(303, "/account/signin"+queryParams) 105 114 } 106 115 107 116 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { ··· 111 120 sess.AddFlash("Something went wrong!", "error") 112 121 } 113 122 sess.Save(e.Request(), e.Response()) 114 - return e.Redirect(303, "/account/signin") 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 + } 115 162 } 116 163 117 164 sess.Options = &sessions.Options{ ··· 127 174 return err 128 175 } 129 176 130 - if req.QueryParams != "" { 131 - return e.Redirect(303, "/oauth/authorize?"+req.QueryParams) 177 + if queryParams != "" { 178 + return e.Redirect(303, "/oauth/authorize"+queryParams) 132 179 } else { 133 180 return e.Redirect(303, "/account") 134 181 }
+8 -8
server/handle_server_create_session.go
··· 87 87 return helpers.ServerError(e, nil) 88 88 } 89 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 + 90 97 // if repo requires auth factor token and one hasn't been provided, return error prompting for one 91 98 if repo.EmailAuthFactor && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 92 99 err = s.createAndSendAuthCode(ctx, repo) ··· 103 110 if repo.AuthCode == nil || repo.AuthCodeExpiresAt == nil { 104 111 err = s.createAndSendAuthCode(ctx, repo) 105 112 if err != nil { 106 - s.logger.Error("sending auth code", "error", err) 113 + logger.Error("sending auth code", "error", err) 107 114 return helpers.ServerError(e, nil) 108 115 } 109 116 ··· 117 124 if time.Now().UTC().After(*repo.AuthCodeExpiresAt) { 118 125 return helpers.ExpiredTokenError(e) 119 126 } 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 127 } 128 128 129 129 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>