+10
-3
models/models.go
+10
-3
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
33
-
AuthCode *string
34
-
AuthCodeExpiresAt *time.Time
39
+
TwoFactorCode *string
40
+
TwoFactorCodeExpiresAt *time.Time
41
+
TwoFactorType TwoFactorType `gorm:"default:none"`
35
42
}
36
43
37
44
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+56
-9
server/handle_account_signin.go
+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
}
+25
-22
server/handle_server_create_session.go
+25
-22
server/handle_server_create_session.go
···
87
87
return helpers.ServerError(e, nil)
88
88
}
89
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)
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 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)
93
100
if err != nil {
94
-
s.logger.Error("sending auth code", "error", err)
101
+
logger.Error("sending 2FA code", "error", err)
95
102
return helpers.ServerError(e, nil)
96
103
}
97
104
98
105
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
99
106
}
100
107
101
-
// if auth factor is required, now check that the one provided is valid
102
-
if repo.EmailAuthFactor {
103
-
if repo.AuthCode == nil || repo.AuthCodeExpiresAt == nil {
104
-
err = s.createAndSendAuthCode(ctx, repo)
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)
105
112
if err != nil {
106
-
s.logger.Error("sending auth code", "error", err)
113
+
logger.Error("sending 2FA code", "error", err)
107
114
return helpers.ServerError(e, nil)
108
115
}
109
116
110
117
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
111
118
}
112
119
113
-
if *repo.AuthCode != *req.AuthFactorToken {
120
+
if *repo.TwoFactorCode != *req.AuthFactorToken {
114
121
return helpers.InvalidTokenError(e)
115
122
}
116
123
117
-
if time.Now().UTC().After(*repo.AuthCodeExpiresAt) {
124
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
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)
···
139
139
Did: repo.Repo.Did,
140
140
Email: repo.Email,
141
141
EmailConfirmed: repo.EmailConfirmedAt != nil,
142
-
EmailAuthFactor: repo.EmailAuthFactor,
142
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
143
143
Active: repo.Active(),
144
144
Status: repo.Status(),
145
145
})
146
146
}
147
147
148
-
func (s *Server) createAndSendAuthCode(ctx context.Context, repo models.RepoActor) error {
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
+
149
152
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
150
153
eat := time.Now().Add(10 * time.Minute).UTC()
151
154
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 {
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 {
153
156
return fmt.Errorf("updating repo: %w", err)
154
157
}
155
158
156
-
if err := s.sendAuthCode(repo.Email, repo.Handle, code); err != nil {
159
+
if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil {
157
160
return fmt.Errorf("sending email: %w", err)
158
161
}
159
162
+1
-1
server/handle_server_get_session.go
+1
-1
server/handle_server_get_session.go
+8
-3
server/handle_server_update_email.go
+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
}
+1
-1
server/mail.go
+1
-1
server/mail.go
+4
server/templates/signin.html
+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>