A password reset that keeps quiet
For the platform’s first ten days, our authentication had no way back in for someone who had forgotten their password. “Password: done” on our checklist had quietly meant sign-up and sign-in, and nobody had asked what happens next. Adding a reset was quick. Doing it properly took longer, because a forgot-password form is one of the easiest places on a website to leak something.
The leak in the friendly message
Picture the form. You type an address and press the button. If the site says “we’ve emailed you” for an address that has an account, and “no account with that address” for one that doesn’t, you have built a tool for finding out who uses the service. Anyone can paste in a list of addresses and read off the answers.
In a market where most apps have small user bases and everyone knows which apps their neighbours use, that list is more than an annoyance. It is an address book of a particular shop’s customers, a particular school’s parents, a particular clinic’s patients. So the rule we wrote down is that the reset answers the same, for an account and for a stranger.
The same answer, and the same time
Same words are not enough. If the request for a real account takes 800 milliseconds because it sends an email, and the request for a stranger takes 20 because it doesn’t, the stopwatch tells the story the message hides. So the answer is decided first, identically, and the actual delivery runs in the background after the response has gone. A caller cannot tell the cases apart by the words or by the clock.
The only refusals we allow are ones that don’t depend on the address: the mail provider isn’t working, or this caller has asked too many times for the same address (ten an hour). Either of those is the same for everybody.
Details that are easy to get wrong
- A reset code and a sign-in code are different things. They share the machinery that makes codes safe (hashed, single-use, five attempts, at most three live per address), but each is looked up by kind. If we simply used “the newest code for this address,” asking for a sign-in code would silently kill a reset link that hadn’t been clicked yet. And a sign-in code cannot be redeemed as a reset.
- A reset never creates an account. It would be an easy mistake to make, since the sign-in flow can.
- The link opens a session on the app’s own site, and the app then asks for a new password. Phone apps that can’t open links use the same code from the email.
- Choosing a new password ends every other session. A reset that leaves an attacker signed in has changed nothing. Your current session is kept, or the reset would end at a sign-in screen.
- A banned account gets no mail, and says nothing about why.
One honest caveat belongs in there. An access token that has already been issued to another session stays valid until it expires, an hour by default. Its refresh token is dead, so it can’t be renewed, but for up to an hour it can still be used.
What it still doesn’t do
Reset by email works where email delivery is switched on. Someone who signs in with only a phone number still has their phone code, and there is no reset by SMS for them yet. We don’t have custom email templates beyond the app’s display name, or custom SMTP. Two-factor sign-in is not built.
The best answer a forgot-password form can give is one that tells an attacker nothing.
The same instinct, answer the same and keep the timing the same, shows up elsewhere in the platform. It costs a little complexity, and it is the difference between a feature that works and one that works safely.