An OAuth login can be carefully designed and still have a weak recovery path. Teams often spend time protecting the authorization code flow, then treat “send a link to this email address” as a harmless utility. It is not harmless. A recovery link can change credentials, create a new session, or connect an external identity provider to an existing account.
The distinction is simple: email proof shows control of a mailbox. It does not prove that the person is the original owner, that the mailbox is private, or that the request is safe to honor.
Email proof is not recovery authorization
The recovery message is one input to an authorization decision, not the decision itself. Ask what action the link permits, which account it targets, and what signals should cancel it.
For example, a token that only lets a user choose a new password should not also silently link a new OAuth provider. Those are different privilege changes. Keeping them separate makes the threat model smaller and the audit trail much easier to read.
This is also where privacy matters. A recovery endpoint should not reveal whether an email belongs to an account. Return the same outward response for known and unknown addresses, and keep sensitive details out of the URL. A user may choose to get temporary email for a test account, but a disposable mailbox is not evidence of identity.
Map the recovery threat model
Before choosing token settings, write down the assets and the people who might get a token they should not have. The list is more practical than a generic “secure the link” requirement:
- Token theft: a link leaks through browser history, a referrer, a support ticket, or an inbox preview.
- Replay: a valid link is used again after the password has already changed.
- Account confusion: the browser is logged into one account while the link was issued for another.
- Mailbox takeover: an attacker controls the mailbox but not the expected user context.
- Abuse at scale: an attacker requests many messages to enumerate users or exhaust a mail queue.
The threat model should be written before code lands, not after the first incident. For each risk, record the prevention, detection signal, and response that revokes outstanding access.
Design a revocable recovery link
Use a random, single-purpose token that represents a server-side recovery transaction. Do not put the user ID, email address, or a long-lived signed object in the URL just because it is convenient. Store a hash of the token, its purpose, target account, creation time, expiry, and consumption state.
The link should expire quick enough to reduce exposure, but not so quickly that users forward it to support in frustration. More important than one universal duration is a clear policy: a new recovery transaction can invalidate older ones, consumption makes a token unusable, and changing the password revokes sessions according to account policy.
A basic server-side sequence looks like this:
- Create a recovery transaction with a narrow purpose and a random secret.
- Store only a digest of the secret, plus the account and expiry metadata.
- Send the raw secret in a link over the normal mail channel.
- On use, atomically mark the transaction consumed before applying the change.
- Revoke sessions, refresh tokens, or linked credentials that the policy says are no longer trustworthy.
- Record an audit event without logging the raw URL or secret.
The atomic consume step matters. Two browser tabs, a retrying client, or a malicious script must not turn one message into two successful password changes. A database uniqueness rule or a compare-and-set update is often clearer than a best-effort flag in application memory.
Recovery workers also need operational boundaries. A short failure budget for recovery jobs helps decide when delayed delivery is preferable to silently dropping messages. Queue-safe password reset emails also show why delivery state and authorization state should stay separate.
Keep delivery and security observable
Logs are usefull only when they answer a security question. Capture a transaction ID, a privacy-conscious account identifier, requested action, result, and rejection reason. Do not capture the token, full recovery URL, or message body.
Alert on patterns rather than single events: repeated requests for many accounts from one source, a successful use from a surprising location after a burst of requests, or many invalid tokens for the same account. Rate limits should protect both the recovery endpoint and the outbound mail system. A generic response prevents enumeration, while internal metrics still show where abuse is happening.
After a password change, tell the user what happened in a neutral notification and offer a clear path to report it. The message should not include the new password, and it should not assume that the recovery email itself is a trusted support channel.
A practical review checklist
- Does the token have one purpose and one account target?
- Is only a token digest stored?
- Are old transactions invalidated when the policy requires it?
- Is consumption atomic and idempotent?
- Does the link reveal no unnecessary personal data?
- Are password, session, and provider-link changes separate actions?
- Do generic responses prevent account enumeration?
- Are retries, rate limits, and delivery failures visible?
- Can support revoke a suspicious transaction without reading the secret?
- Are audit events useful without becoming another secret store?
One small wording detail is worth testing too. Search and support tickets contain messy phrases such as “fake e mail com”; your detection and documentation should not mistake that text for a security control. The control is the transaction policy behind the link.
Questions teams usually ask
Should every new recovery request invalidate the previous one?
Usually yes for a simple account model, because it reduces the number of live secrets. There are exceptions for carefully designed multi-device flows, but they need an explicit user experience and stronger audit trail.
Is a signed JWT enough for a recovery link?
Not by itself. A signature can prove who issued a token, but it does not automatically provide one-time use, server-side revocation, or protection against a token copied from a browser. A stateful transaction is often easier to revoke and explain.
What happens when a user reports an unexpected message?
Revoke outstanding recovery transactions, review recent account changes, and provide a safe re-authentication path. Avoid asking the user to forward the original link, since that can spread a still-valid secret.
The safest recovery flow is not the one with the most cryptography in it. It is the one whose permissions, expiry, revocation, delivery, and evidence are all explicit enough for the next engineer to reason about them.
Top comments (0)