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

refactor so that there's a 2FA type on the repo which replaces EmailAuthFactor

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

+10 -3
models/models.go
··· 8 "github.com/bluesky-social/indigo/atproto/atcrypto" 9 ) 10 11 type Repo struct { 12 Did string `gorm:"primaryKey"` 13 CreatedAt time.Time ··· 29 Root []byte 30 Preferences []byte 31 Deactivated bool 32 - EmailAuthFactor bool 33 - AuthCode *string 34 - AuthCodeExpiresAt *time.Time 35 } 36 37 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
··· 8 "github.com/bluesky-social/indigo/atproto/atcrypto" 9 ) 10 11 + type TwoFactorType string 12 + 13 + var ( 14 + TwoFactorTypeNone = TwoFactorType("none") 15 + TwoFactorTypeEmail = TwoFactorType("email") 16 + ) 17 + 18 type Repo struct { 19 Did string `gorm:"primaryKey"` 20 CreatedAt time.Time ··· 36 Root []byte 37 Preferences []byte 38 Deactivated bool 39 + TwoFactorCode *string 40 + TwoFactorCodeExpiresAt *time.Time 41 + TwoFactorType TwoFactorType `gorm:"default:none"` 42 } 43 44 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+9 -9
server/handle_account_signin.go
··· 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()) ··· 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()) ··· 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 }
··· 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()) ··· 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()) ··· 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 } 162 }
+18 -15
server/handle_server_create_session.go
··· 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) 100 if err != nil { 101 - s.logger.Error("sending auth code", "error", err) 102 return helpers.ServerError(e, nil) 103 } 104 105 return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 106 } 107 108 - // if auth factor is required, now check that the one provided is valid 109 - if repo.EmailAuthFactor { 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 117 return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 118 } 119 120 - if *repo.AuthCode != *req.AuthFactorToken { 121 return helpers.InvalidTokenError(e) 122 } 123 124 - if time.Now().UTC().After(*repo.AuthCodeExpiresAt) { 125 return helpers.ExpiredTokenError(e) 126 } 127 } ··· 139 Did: repo.Repo.Did, 140 Email: repo.Email, 141 EmailConfirmed: repo.EmailConfirmedAt != nil, 142 - EmailAuthFactor: repo.EmailAuthFactor, 143 Active: repo.Active(), 144 Status: repo.Status(), 145 }) 146 } 147 148 - func (s *Server) createAndSendAuthCode(ctx context.Context, repo models.RepoActor) error { 149 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 150 eat := time.Now().Add(10 * time.Minute).UTC() 151 152 - if err := s.db.Exec(ctx, "UPDATE repos SET auth_code = ?, auth_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil { 153 return fmt.Errorf("updating repo: %w", err) 154 } 155 156 - if err := s.sendAuthCode(repo.Email, repo.Handle, code); err != nil { 157 return fmt.Errorf("sending email: %w", err) 158 } 159
··· 94 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 95 } 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 } ··· 139 Did: repo.Repo.Did, 140 Email: repo.Email, 141 EmailConfirmed: repo.EmailConfirmedAt != nil, 142 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 143 Active: repo.Active(), 144 Status: repo.Status(), 145 }) 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
+1 -1
server/handle_server_get_session.go
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 - EmailAuthFactor: repo.EmailAuthFactor, 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
+8 -3
server/handle_server_update_email.go
··· 33 // To disable email auth factor a token is required. 34 // To enable email auth factor a token is not required. 35 // If updating an email address, a token will be sent anyway 36 - if urepo.EmailAuthFactor && req.EmailAuthFactor == false && req.Token == "" { 37 return helpers.InvalidTokenError(e) 38 } 39 ··· 51 } 52 } 53 54 - query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_auth_factor = ?, email = ?" 55 56 if urepo.Email != req.Email { 57 query += ",email_confirmed_at = NULL" ··· 59 60 query += " WHERE did = ?" 61 62 - if err := s.db.Exec(ctx, query, nil, req.EmailAuthFactor, req.Email, urepo.Repo.Did).Error; err != nil { 63 logger.Error("error updating repo", "error", err) 64 return helpers.ServerError(e, nil) 65 }
··· 33 // To disable email auth factor a token is required. 34 // To enable email auth factor a token is not required. 35 // If updating an email address, a token will be sent anyway 36 + if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" { 37 return helpers.InvalidTokenError(e) 38 } 39 ··· 51 } 52 } 53 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 = ?" 60 61 if urepo.Email != req.Email { 62 query += ",email_confirmed_at = NULL" ··· 64 65 query += " WHERE did = ?" 66 67 + if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil { 68 logger.Error("error updating repo", "error", err) 69 return helpers.ServerError(e, nil) 70 }
+1 -1
server/mail.go
··· 97 return nil 98 } 99 100 - func (s *Server) sendAuthCode(email, handle, code string) error { 101 if s.mail == nil { 102 return nil 103 }
··· 97 return nil 98 } 99 100 + func (s *Server) sendTwoFactorCode(email, handle, code string) error { 101 if s.mail == nil { 102 return nil 103 }