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

Skip to content

Commit 7a5ae1e

Browse files
authored
fix: delete all sessions on password change (coder#4659)
- Prevent users from reusing their old password as their new password.
1 parent ea156cc commit 7a5ae1e

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

coderd/database/databasefake/databasefake.go

+13
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,19 @@ func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error {
368368
return sql.ErrNoRows
369369
}
370370

371+
func (q *fakeQuerier) DeleteAPIKeysByUserID(_ context.Context, userID uuid.UUID) error {
372+
q.mutex.Lock()
373+
defer q.mutex.Unlock()
374+
375+
for i := len(q.apiKeys) - 1; i >= 0; i-- {
376+
if q.apiKeys[i].UserID == userID {
377+
q.apiKeys = append(q.apiKeys[:i], q.apiKeys[i+1:]...)
378+
}
379+
}
380+
381+
return nil
382+
}
383+
371384
func (q *fakeQuerier) GetFileByHashAndCreator(_ context.Context, arg database.GetFileByHashAndCreatorParams) (database.File, error) {
372385
q.mutex.RLock()
373386
defer q.mutex.RUnlock()

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/apikeys.sql

+6
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,9 @@ FROM
5454
api_keys
5555
WHERE
5656
id = $1;
57+
58+
-- name: DeleteAPIKeysByUserID :exec
59+
DELETE FROM
60+
api_keys
61+
WHERE
62+
user_id = $1;

coderd/users.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,14 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
632632
}
633633
}
634634

635+
// Prevent users reusing their old password.
636+
if match, _ := userpassword.Compare(string(user.HashedPassword), params.Password); match {
637+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
638+
Message: "New password cannot match old password.",
639+
})
640+
return
641+
}
642+
635643
hashedPassword, err := userpassword.Hash(params.Password)
636644
if err != nil {
637645
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -640,9 +648,22 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
640648
})
641649
return
642650
}
643-
err = api.Database.UpdateUserHashedPassword(ctx, database.UpdateUserHashedPasswordParams{
644-
ID: user.ID,
645-
HashedPassword: []byte(hashedPassword),
651+
652+
err = api.Database.InTx(func(tx database.Store) error {
653+
err = tx.UpdateUserHashedPassword(ctx, database.UpdateUserHashedPasswordParams{
654+
ID: user.ID,
655+
HashedPassword: []byte(hashedPassword),
656+
})
657+
if err != nil {
658+
return xerrors.Errorf("update user hashed password: %w", err)
659+
}
660+
661+
err = tx.DeleteAPIKeysByUserID(ctx, user.ID)
662+
if err != nil {
663+
return xerrors.Errorf("delete api keys by user ID: %w", err)
664+
}
665+
666+
return nil
646667
})
647668
if err != nil {
648669
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{

coderd/users_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,67 @@ func TestUpdateUserPassword(t *testing.T) {
644644
assert.Len(t, auditor.AuditLogs, 1)
645645
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[0].Action)
646646
})
647+
648+
t.Run("ChangingPasswordDeletesKeys", func(t *testing.T) {
649+
t.Parallel()
650+
651+
client := coderdtest.New(t, nil)
652+
user := coderdtest.CreateFirstUser(t, client)
653+
ctx, _ := testutil.Context(t)
654+
655+
apikey1, err := client.CreateToken(ctx, user.UserID.String(), codersdk.CreateTokenRequest{})
656+
require.NoError(t, err)
657+
658+
apikey2, err := client.CreateToken(ctx, user.UserID.String(), codersdk.CreateTokenRequest{})
659+
require.NoError(t, err)
660+
661+
err = client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
662+
Password: "newpassword",
663+
})
664+
require.NoError(t, err)
665+
666+
// Trying to get an API key should fail since our client's token
667+
// has been deleted.
668+
_, err = client.GetAPIKey(ctx, user.UserID.String(), apikey1.Key)
669+
require.Error(t, err)
670+
cerr := coderdtest.SDKError(t, err)
671+
require.Equal(t, http.StatusUnauthorized, cerr.StatusCode())
672+
673+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
674+
Email: coderdtest.FirstUserParams.Email,
675+
Password: "newpassword",
676+
})
677+
require.NoError(t, err)
678+
679+
client.SessionToken = resp.SessionToken
680+
681+
// Trying to get an API key should fail since all keys are deleted
682+
// on password change.
683+
_, err = client.GetAPIKey(ctx, user.UserID.String(), apikey1.Key)
684+
require.Error(t, err)
685+
cerr = coderdtest.SDKError(t, err)
686+
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
687+
688+
_, err = client.GetAPIKey(ctx, user.UserID.String(), apikey2.Key)
689+
require.Error(t, err)
690+
cerr = coderdtest.SDKError(t, err)
691+
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
692+
})
693+
694+
t.Run("PasswordsMustDiffer", func(t *testing.T) {
695+
t.Parallel()
696+
697+
client := coderdtest.New(t, nil)
698+
_ = coderdtest.CreateFirstUser(t, client)
699+
ctx, _ := testutil.Context(t)
700+
701+
err := client.UpdateUserPassword(ctx, "me", codersdk.UpdateUserPasswordRequest{
702+
Password: coderdtest.FirstUserParams.Password,
703+
})
704+
require.Error(t, err)
705+
cerr := coderdtest.SDKError(t, err)
706+
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
707+
})
647708
}
648709

649710
func TestGrantSiteRoles(t *testing.T) {

0 commit comments

Comments
 (0)