feat: implement api for "forgot password?" flow#14915
Conversation
|
What does this look like in the settings? I know we want to avoid adding notification-specific functionality but I'm not sure that this should appear in a "Notification Preferences" table. Users probably shouldn't be able to disable the flow at all. |
It appears like the rest of the notifications. I definitely agree that a user shouldn't be able to disable it. I'm happy to look into stopping it from being displayed on this page. @dannykopping What're your thoughts on this? |
This case is easily fixable and diagnosable, so I think special-casing is not worth the extra complexity. |
|
To add some more context: In my experience, special-casing ends up rotting over time. I definitely understand the motivation here though @stirby, and it will indeed be quite a simple special-casing, so if you feel strongly about it then let's do it. I think the back-end should be responsibly for hiding that option, and it should be done based on the template ID (not the name). |
dannykopping
left a comment
There was a problem hiding this comment.
I appreciate your security awareness here, and the methodical approach.
There are a few things to fix, but nothing major.
|
|
||
| if equal, _ = userpassword.Compare(string(user.HashedPassword), req.Password); equal { | ||
| httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ | ||
| Message: "New password cannot match old password.", |
There was a problem hiding this comment.
I'm curious: why not? If they're forgotten their password but have now somehow remembered it, do we have to force a change?
There was a problem hiding this comment.
I'm happy to go either way on this. The decision to do this was based on similar logic being performed on the putUserPassword endpoint.
Lines 1048 to 1054 in 32a8df4
There was a problem hiding this comment.
Did we come to a decision on this?
| return xerrors.Errorf("update user hashed password: %w", err) | ||
| } | ||
|
|
||
| //nolint:gocritic // We need to delete all API keys for the user. |
There was a problem hiding this comment.
Why? API keys are separate authorization contexts to user passwords.
For example, as an operator I might've created an API key to use in CI to do x. It would be pretty weird and unexpected to invalidate that key if I forgot my personal password.
I'm not against this, but the reasons need to be carefully articulated and documented. When in doubt, follow the principle of least surprise.
There was a problem hiding this comment.
The putUserPassword endpoint has this behaviour. My assumption is that we'd want the behaviour to match. Unfortunately there are no comments in the immediate area justifying the decision.
Line 977 in 32a8df4
Lines 1074 to 1077 in 32a8df4
I'm happy to have a discussion around whether this should stay or go.
There was a problem hiding this comment.
Hhmm, do we document this behaviour anywhere?
There was a problem hiding this comment.
If we do I am unable to find it.
There was a problem hiding this comment.
Given that this behaviour exists, we can't change it now - and I think that it'd be the expectation of operators for this to behave similarly.
I do think we should document this explicitly (could be done out-of-band of this PR), and raise an issue to clarify why we do this and possibly remove it for the reasons I outlined initially.
There was a problem hiding this comment.
@DanielleMaywood would you mind creating an issue for follow-up?
mafredri
left a comment
There was a problem hiding this comment.
Nice work! Had a few suggestions, random thoughts and one larger concern.
| } | ||
| aReq.Old = user | ||
|
|
||
| passcode := uuid.New() |
There was a problem hiding this comment.
Side-note: I always feel weirded out using plain UUIDs for auth, but we do it elsewhere and UUIDv4 is supposedly OK. There are varying views on this though. I bring this up because I wonder if we should consider using something with more entropy in the future. 🤔
There was a problem hiding this comment.
(According to ChatGPT o1-preview) there are 122 bits of entropy in UUIDv4, isn't that enough for you? 😆
There was a problem hiding this comment.
Also worth noting, the UUIDv4 implementation we use does use a cryptographically secure RNG.
Line 122 in 764b0cc
https://github.com/google/uuid/blob/0e97ed3b537927cb4afea366bc4cc36f6eb37e75/uuid.go#L45
https://github.com/google/uuid/blob/0e97ed3b537927cb4afea366bc4cc36f6eb37e75/uuid.go#L9
There was a problem hiding this comment.
@dannykopping exactly, it's a few bits short. I believe 128 or more bits of entropy is the recommendation (required by RFC6749, for example). 😄
@DanielleMaywood yes, definitely worth noting 👍🏻
There was a problem hiding this comment.
In that case, should we go for an alternative to using a UUID here?
There was a problem hiding this comment.
@mtojek and I discussed this and have decided that the current solution should be sufficient. The tokens are generated with a cryptographically secure RNG, the rate limiter is pretty strict (60req/s) and tokens do not last very long so the few bits short in entropy should be sufficient. We can always review this in the future if we decide a different approach should be taken.
| } | ||
| aReq.Old = user | ||
|
|
||
| equal, err := userpassword.Compare(string(user.HashedOneTimePasscode), req.OneTimePasscode) |
There was a problem hiding this comment.
What happens here when you give a valid user email, but the token is null? And where do we verify the expiry of the token?
There was a problem hiding this comment.
Oh I've completely forgotten to check the expiry 🤦♀️ Thank you for spotting that. Will definitely get that added.
There was a problem hiding this comment.
I've now added a test to verify that tokens do expire.
| } | ||
|
|
||
| //nolint:gocritic // We need the system auth context to be able to update the user's password. | ||
| err = tx.UpdateUserHashedPassword(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedPasswordParams{ |
There was a problem hiding this comment.
I wish we could do this without system context somehow, seems like a potential security risk.
|
|
||
| notif := notifyEnq.Sent[0] | ||
| require.NotEqual(t, notifications.TemplateUserRequestedOneTimePasscode, notif.TemplateID) | ||
| }) |
There was a problem hiding this comment.
A few more test-cases could be good, like expired token and resetting the pw of a user that hasn't requested one.
| } | ||
| aReq.Old = user | ||
|
|
||
| passcode := uuid.New() |
There was a problem hiding this comment.
(According to ChatGPT o1-preview) there are 122 bits of entropy in UUIDv4, isn't that enough for you? 😆
mtojek
left a comment
There was a problem hiding this comment.
Last to the party, but added a few cents 👍
Nice work, Danielle!
mafredri
left a comment
There was a problem hiding this comment.
Looking great! Approving since I think the remaining stuff is pretty minor mainly documenting the reasoning and adding one more test.
|
Great work @DanielleMaywood! |

Relates to #14232
This implements two endpoints (names subject to change):
/api/v2/users/otp/request/api/v2/users/otp/change-passwordThey are both in the same group as
/api/v2/users/loginso they have the same rate limiting logic applied.