- Notifications
You must be signed in to change notification settings - Fork928
feat: implement api for "forgot password?" flow#14915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I appreciate your security awareness here, and the methodical approach.
There are a few things to fix, but nothing major.
coderd/database/migrations/000260_notifications_forgot_password.up.sql OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
coderd/database/migrations/000260_notifications_forgot_password.up.sql OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
coderd/userauth.go Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'm happy to go either way on this. The decision to do this was based on similar logic being performed on theputUserPassword
endpoint.
Lines 1048 to 1054 in32a8df4
// Prevent users reusing their old password. | |
ifmatch,_:=userpassword.Compare(string(user.HashedPassword),params.Password);match { | |
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{ | |
Message:"New password cannot match old password.", | |
}) | |
return | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
WDYT@stirby?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Did we come to a decision on this?
coderd/userauth.go Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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 dox
. 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 articulatedand documented. When in doubt, follow the principle of least surprise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
TheputUserPassword
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 in32a8df4
func (api*API)putUserPassword(rw http.ResponseWriter,r*http.Request) { |
Lines 1074 to 1077 in32a8df4
err=tx.DeleteAPIKeysByUserID(ctx,user.ID) | |
iferr!=nil { | |
returnxerrors.Errorf("delete api keys by user ID: %w",err) | |
} |
I'm happy to have a discussion around whether this should stay or go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Hhmm, do we document this behaviour anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
If we do I am unable to find it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Ido 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@DanielleMaywood would you mind creating an issue for follow-up?
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Nice work! Had a few suggestions, random thoughts and one larger concern.
Uh oh!
There was an error while loading.Please reload this page.
} | ||
aReq.Old = user | ||
passcode := uuid.New() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Also worth noting, the UUIDv4 implementation we use does use a cryptographically secure RNG.
Line 122 in764b0cc
github.com/google/uuidv1.6.0 |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@dannykopping exactly, it's a few bits short. I believe 128 or more bits of entropy is the recommendation (required byRFC6749, for example). 😄
@DanielleMaywood yes, definitely worth noting 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ha! TIL 🥲
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
In that case, should we go for an alternative to using a UUID here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@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 entropyshould be sufficient. We can always review this in the future if we decide a different approach should be taken.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
} | ||
aReq.Old = user | ||
equal, err := userpassword.Compare(string(user.HashedOneTimePasscode), req.OneTimePasscode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
A few more test-cases could be good, like expired token and resetting the pw of a user that hasn't requested one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Can do!
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
} | ||
aReq.Old = user | ||
passcode := uuid.New() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
(According to ChatGPT o1-preview) there are 122 bits of entropy in UUIDv4, isn't that enough for you? 😆
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Last to the party, but added a few cents 👍
Nice work, Danielle!
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
...otifications/testdata/rendered-templates/TemplateUserRequestedOneTimePasscode-body.md.goldenShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
...tifications/testdata/rendered-templates/TemplateUserRequestedOneTimePasscode-title.md.golden OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Perfect 👍 I think it is in good shape to merge, but let's wait for@mafredri 's approval as Danny is out.
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Looking great! Approving since I think the remaining stuff is pretty minor mainly documenting the reasoning and adding one more test.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
...otifications/testdata/rendered-templates/TemplateUserRequestedOneTimePasscode-body.md.goldenShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
4369f2b
intomainUh oh!
There was an error while loading.Please reload this page.
Great work@DanielleMaywood! |
Uh oh!
There was an error while loading.Please reload this page.
Relates to#14232
This implements two endpoints (names subject to change):
/api/v2/users/otp/request
/api/v2/users/otp/change-password
They are both in the same group as
/api/v2/users/login
so they have the same rate limiting logic applied.