Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4369f2b

Browse files
feat: implement api for "forgot password?" flow (#14915)
Relates to #14232 This implements two endpoints (names subject to change): - `/api/v2/users/otp/request` - `/api/v2/users/otp/change-password`
1 parent 8785a51 commit 4369f2b

25 files changed

+1007
-4
lines changed

coderd/apidoc/docs.go

Lines changed: 88 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ type Options struct {
248248

249249
// IDPSync holds all configured values for syncing external IDP users into Coder.
250250
IDPSync idpsync.IDPSync
251+
252+
// OneTimePasscodeValidityPeriod specifies how long a one time passcode should be valid for.
253+
OneTimePasscodeValidityPeriod time.Duration
251254
}
252255

253256
// @title Coder API
@@ -387,6 +390,9 @@ func New(options *Options) *API {
387390
v := schedule.NewAGPLUserQuietHoursScheduleStore()
388391
options.UserQuietHoursScheduleStore.Store(&v)
389392
}
393+
if options.OneTimePasscodeValidityPeriod == 0 {
394+
options.OneTimePasscodeValidityPeriod = 20 * time.Minute
395+
}
390396

391397
if options.StatsBatcher == nil {
392398
panic("developer error: options.StatsBatcher is nil")
@@ -984,6 +990,8 @@ func New(options *Options) *API {
984990
// This value is intentionally increased during tests.
985991
r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute))
986992
r.Post("/login", api.postLogin)
993+
r.Post("/otp/request", api.postRequestOneTimePasscode)
994+
r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode)
987995
r.Route("/oauth2", func(r chi.Router) {
988996
r.Route("/github", func(r chi.Router) {
989997
r.Use(

coderd/coderdtest/coderdtest.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ type Options struct {
128128
LoginRateLimit int
129129
FilesRateLimit int
130130

131+
// OneTimePasscodeValidityPeriod specifies how long a one time passcode should be valid for.
132+
OneTimePasscodeValidityPeriod time.Duration
133+
131134
// IncludeProvisionerDaemon when true means to start an in-memory provisionerD
132135
IncludeProvisionerDaemon bool
133136
ProvisionerDaemonTags map[string]string
@@ -311,6 +314,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
311314
options.NotificationsEnqueuer = &testutil.FakeNotificationsEnqueuer{}
312315
}
313316

317+
if options.OneTimePasscodeValidityPeriod == 0 {
318+
options.OneTimePasscodeValidityPeriod = testutil.WaitLong
319+
}
320+
314321
var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
315322
if options.TemplateScheduleStore == nil {
316323
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
@@ -530,6 +537,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
530537
DatabaseRolluper: options.DatabaseRolluper,
531538
WorkspaceUsageTracker: wuTracker,
532539
NotificationsEnqueuer: options.NotificationsEnqueuer,
540+
OneTimePasscodeValidityPeriod: options.OneTimePasscodeValidityPeriod,
533541
}
534542
}
535543

coderd/coderdtest/swaggerparser.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
303303
if comment.router == "/updatecheck" ||
304304
comment.router == "/buildinfo" ||
305305
comment.router == "/" ||
306-
comment.router == "/users/login" {
306+
comment.router == "/users/login" ||
307+
comment.router == "/users/otp/request" ||
308+
comment.router == "/users/otp/change-password" {
307309
return // endpoints do not require authorization
308310
}
309311
assert.Equal(t, "CoderSessionToken", comment.security, "@Security must be equal CoderSessionToken")

coderd/database/dbauthz/dbauthz.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3628,6 +3628,14 @@ func (q *querier) UpdateUserGithubComUserID(ctx context.Context, arg database.Up
36283628
return q.db.UpdateUserGithubComUserID(ctx, arg)
36293629
}
36303630

3631+
func (q *querier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error {
3632+
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
3633+
return err
3634+
}
3635+
3636+
return q.db.UpdateUserHashedOneTimePasscode(ctx, arg)
3637+
}
3638+
36313639
func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
36323640
user, err := q.db.GetUserByID(ctx, arg.ID)
36333641
if err != nil {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,12 @@ func (s *MethodTestSuite) TestUser() {
11871187
ID: u.ID,
11881188
}).Asserts(u, policy.ActionUpdatePersonal).Returns()
11891189
}))
1190+
s.Run("UpdateUserHashedOneTimePasscode", s.Subtest(func(db database.Store, check *expects) {
1191+
u := dbgen.User(s.T(), db, database.User{})
1192+
check.Args(database.UpdateUserHashedOneTimePasscodeParams{
1193+
ID: u.ID,
1194+
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns()
1195+
}))
11901196
s.Run("UpdateUserQuietHoursSchedule", s.Subtest(func(db database.Store, check *expects) {
11911197
u := dbgen.User(s.T(), db, database.User{})
11921198
check.Args(database.UpdateUserQuietHoursScheduleParams{

coderd/database/dbmem/dbmem.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9077,6 +9077,26 @@ func (q *FakeQuerier) UpdateUserGithubComUserID(_ context.Context, arg database.
90779077
return sql.ErrNoRows
90789078
}
90799079

9080+
func (q *FakeQuerier) UpdateUserHashedOneTimePasscode(_ context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error {
9081+
err := validateDatabaseType(arg)
9082+
if err != nil {
9083+
return err
9084+
}
9085+
9086+
q.mutex.Lock()
9087+
defer q.mutex.Unlock()
9088+
9089+
for i, user := range q.users {
9090+
if user.ID != arg.ID {
9091+
continue
9092+
}
9093+
user.HashedOneTimePasscode = arg.HashedOneTimePasscode
9094+
user.OneTimePasscodeExpiresAt = arg.OneTimePasscodeExpiresAt
9095+
q.users[i] = user
9096+
}
9097+
return nil
9098+
}
9099+
90809100
func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
90819101
if err := validateDatabaseType(arg); err != nil {
90829102
return err
@@ -9090,6 +9110,8 @@ func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.U
90909110
continue
90919111
}
90929112
user.HashedPassword = arg.HashedPassword
9113+
user.HashedOneTimePasscode = nil
9114+
user.OneTimePasscodeExpiresAt = sql.NullTime{}
90939115
q.users[i] = user
90949116
return nil
90959117
}

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = '62f86a30-2330-4b61-a26d-311ff3b608cf';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
2+
VALUES ('62f86a30-2330-4b61-a26d-311ff3b608cf', 'One-Time Passcode', E'Your One-Time Passcode for Coder.',
3+
E'Hi {{.UserName}},\n\nA request to reset the password for your Coder account has been made. Your one-time passcode is:\n\n**{{.Labels.one_time_passcode}}**\n\nIf you did not request to reset your password, you can ignore this message.',
4+
'User Events', '[]'::jsonb);

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)