Password reset is broken — Solution
Enterprise users received password-reset emails normally, but the links were already marked as used when customers opened them. The failure was caused by mail-security link inspection reaching a state-changing GET before message delivery.
1. Reproduce the provider split
Send reset links to both test mailbox types in the Incident Workbench:
Northwind SecureMailreturns This password reset link has already been used.Postbird Mailopens the form and completes normally.
The provider is the meaningful variable. Token generation, email delivery, and service health are all working.
2. Put the evidence in chronological order
The production trace for the failing enterprise mailbox shows:
10:14:03.000 notification-service email accepted by SecureMail
10:14:05.184 accounts-web GET /reset/:token -> 200, token consumed
10:14:06.020 notification-service message delivered to mailbox
10:16:21.000 accounts-web customer GET -> 410 already used
The token-changing request happened before the email reached the customer. That rules out an impatient double-click and points to infrastructure between the sender and inbox.
3. Identify the first visitor correctly
The first request uses a browser-like user agent, but it requests no page assets. The later customer request loads the normal page assets.
Combined with the Secure Link Compatibility note, this identifies the first visitor as the enterprise mail gateway's link inspector. Scanner fingerprinting is not a durable fix: products change identifiers, some copy browser user agents, and legitimate accessibility clients can look unusual too.
4. Find the regression
Release accounts-web 6.14.0 changed link opening from a read-only operation into a token exchange:
GET /reset/:token
validate token
consume token
create browser-bound redemption session
render form
That violates the HTTP safety expectation for GET. Any previewer, scanner, prefetcher, or assistive client can consume the one-time credential without submitting a new password.
Longer token lifetimes do not help because the token is used, not expired. Reusable tokens weaken replay protection. Blocking a known scanner only moves the failure to the next scanner.
5. Make link opening read-only
The form handler should validate the token and render the page without consuming anything or creating a browser-bound session:
@router.get("/reset/{token}")
async def reset_form(request: Request, token: str):
await reset_tokens.require_valid(token)
return templates.render(
"reset-form.html",
request=request,
token=token,
)
Mail-security inspection can now open the link harmlessly. The customer can still use the same token later.
6. Consume the token with the password change
Move the one-time claim to the password-submission handler and perform it in the same transaction as the credential update:
@router.post("/reset/{token}/complete", status_code=204)
async def complete_reset(request: Request, token: str, password: str = Form()):
async with db.transaction() as tx:
reset = await tx.reset_tokens.consume_if_unused(token)
await tx.accounts.update_password(reset.user_id, password)
return Response(status_code=204)
The atomic transaction matters. If token consumption and password update are separate operations, two concurrent submissions can both pass the unused-token check. With one transaction, exactly one submission claims the token and updates the password.
The browser-bound redemption-session dependency is no longer necessary and should be removed.
7. Deploy and verify the invariants
The repaired handler must pass every deployment scenario:
- Standard password reset
- Known SecureMail link inspection
- An unknown scanner fingerprint
- Replay after a successful reset
- Concurrent password submissions
- No-JavaScript and assistive clients
The durable rule is simple: opening a reset link may validate and render, but only the password-changing POST may consume the one-time token—and the claim and credential write must be atomic.