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

Compare changes

Choose any two refs to compare.

+10 -1
models/models.go
··· 8 8 "github.com/bluesky-social/indigo/atproto/atcrypto" 9 9 ) 10 10 11 + type TwoFactorType string 12 + 13 + var ( 14 + TwoFactorTypeNone = TwoFactorType("none") 15 + TwoFactorTypeEmail = TwoFactorType("email") 16 + ) 17 + 11 18 type Repo struct { 12 19 Did string `gorm:"primaryKey"` 13 20 CreatedAt time.Time ··· 29 36 Root []byte 30 37 Preferences []byte 31 38 Deactivated bool 32 - EmailAuthFactor bool 39 + TwoFactorCode *string 40 + TwoFactorCodeExpiresAt *time.Time 41 + TwoFactorType TwoFactorType `gorm:"default:none"` 33 42 } 34 43 35 44 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+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 2FA token and one hasn't been provided, return error prompting for one 127 + if repo.TwoFactorType != models.TwoFactorTypeNone && req.AuthFactorToken == "" { 128 + err = s.createAndSendTwoFactorCode(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 2FAis required, now check that the one provided is valid 141 + if repo.TwoFactorType != models.TwoFactorTypeNone { 142 + if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil { 143 + err = s.createAndSendTwoFactorCode(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.TwoFactorCode != req.AuthFactorToken { 156 + return helpers.InvalidTokenError(e) 157 + } 158 + 159 + if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) { 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 }
+54 -1
server/handle_server_create_session.go
··· 1 1 package server 2 2 3 3 import ( 4 + "context" 4 5 "errors" 6 + "fmt" 5 7 "strings" 8 + "time" 6 9 7 10 "github.com/Azure/go-autorest/autorest/to" 8 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 91 94 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 92 95 } 93 96 97 + // if repo requires 2FA token and one hasn't been provided, return error prompting for one 98 + if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 99 + err = s.createAndSendTwoFactorCode(ctx, repo) 100 + if err != nil { 101 + logger.Error("sending 2FA code", "error", err) 102 + return helpers.ServerError(e, nil) 103 + } 104 + 105 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 106 + } 107 + 108 + // if 2FA is required, now check that the one provided is valid 109 + if repo.TwoFactorType != models.TwoFactorTypeNone { 110 + if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil { 111 + err = s.createAndSendTwoFactorCode(ctx, repo) 112 + if err != nil { 113 + logger.Error("sending 2FA code", "error", err) 114 + return helpers.ServerError(e, nil) 115 + } 116 + 117 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 118 + } 119 + 120 + if *repo.TwoFactorCode != *req.AuthFactorToken { 121 + return helpers.InvalidTokenError(e) 122 + } 123 + 124 + if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) { 125 + return helpers.ExpiredTokenError(e) 126 + } 127 + } 128 + 94 129 sess, err := s.createSession(ctx, &repo.Repo) 95 130 if err != nil { 96 131 logger.Error("error creating session", "error", err) ··· 104 139 Did: repo.Repo.Did, 105 140 Email: repo.Email, 106 141 EmailConfirmed: repo.EmailConfirmedAt != nil, 107 - EmailAuthFactor: repo.EmailAuthFactor, 142 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 108 143 Active: repo.Active(), 109 144 Status: repo.Status(), 110 145 }) 111 146 } 147 + 148 + func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error { 149 + // TODO: when implementing a new type of 2FA there should be some logic in here to send the 150 + // right type of code 151 + 152 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 153 + eat := time.Now().Add(10 * time.Minute).UTC() 154 + 155 + if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil { 156 + return fmt.Errorf("updating repo: %w", err) 157 + } 158 + 159 + if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil { 160 + return fmt.Errorf("sending email: %w", err) 161 + } 162 + 163 + return nil 164 + }
+1 -1
server/handle_server_get_session.go
··· 23 23 Did: repo.Repo.Did, 24 24 Email: repo.Email, 25 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 - EmailAuthFactor: repo.EmailAuthFactor, 26 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 27 27 Active: repo.Active(), 28 28 Status: repo.Status(), 29 29 })
+8 -3
server/handle_server_update_email.go
··· 33 33 // To disable email auth factor a token is required. 34 34 // To enable email auth factor a token is not required. 35 35 // If updating an email address, a token will be sent anyway 36 - if urepo.EmailAuthFactor && req.EmailAuthFactor == false && req.Token == "" { 36 + if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" { 37 37 return helpers.InvalidTokenError(e) 38 38 } 39 39 ··· 51 51 } 52 52 } 53 53 54 - query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_auth_factor = ?, email = ?" 54 + twoFactorType := models.TwoFactorTypeNone 55 + if req.EmailAuthFactor { 56 + twoFactorType = models.TwoFactorTypeEmail 57 + } 58 + 59 + query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, two_factor_type = ?, email = ?" 55 60 56 61 if urepo.Email != req.Email { 57 62 query += ",email_confirmed_at = NULL" ··· 59 64 60 65 query += " WHERE did = ?" 61 66 62 - if err := s.db.Exec(ctx, query, nil, req.EmailAuthFactor, req.Email, urepo.Repo.Did).Error; err != nil { 67 + if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil { 63 68 logger.Error("error updating repo", "error", err) 64 69 return helpers.ServerError(e, nil) 65 70 }
+19
server/mail.go
··· 96 96 97 97 return nil 98 98 } 99 + 100 + func (s *Server) sendTwoFactorCode(email, handle, code string) error { 101 + if s.mail == nil { 102 + return nil 103 + } 104 + 105 + s.mailLk.Lock() 106 + defer s.mailLk.Unlock() 107 + 108 + s.mail.To(email) 109 + s.mail.Subject("2FA code for " + s.config.Hostname) 110 + s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your 2FA code is %s. This code will expire in ten minutes.", handle, code)) 111 + 112 + if err := s.mail.Send(); err != nil { 113 + return err 114 + } 115 + 116 + return nil 117 + }
+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>