Password Reset
RESTHeartThe password reset flow is split into two endpoints: one to request a reset link and one to apply the new password.
The flow is designed to prevent account enumeration: the first endpoint always returns 202 Accepted regardless of whether the email exists.
Request a reset link
POST /auth/forgot-password — publicly accessible ($unauthenticated).
Request
POST /auth/forgot-password
Content-Type: application/json
{ "email": "alice@example.com" }
Server-side steps
-
Validate the email format.
-
Look up the user by email (result is not revealed to the caller).
-
If the user exists and
statusisactive:-
Generate a cryptographically random
passwordResetToken(256-bit) and recordpasswordResetCreatedAt. -
Send a reset email containing a one-time link:
{baseAppUrl}/reset-password?email=alice@example.com&token=<passwordResetToken> -
TTL:
passwordResetTokenTtlHours(default 1 hour).
-
-
Always return
202 Accepted— even if the email is unknown or the user isinvited/pending_verification.
|
Note
|
Responding 202 unconditionally prevents an attacker from enumerating registered email addresses by observing response differences.
|
Response
{ "message": "If that address is registered, a reset link has been sent." }
Apply the new password
PATCH /auth/reset-password — publicly accessible ($unauthenticated).
Request
PATCH /auth/reset-password
Content-Type: application/json
{
"email": "alice@example.com",
"passwordResetToken": "<token from email>",
"password": "new-correct-horse-battery"
}
Server-side steps
-
Validate all required fields.
-
Find user by
email. -
Compare
passwordResetTokenusing constant-time comparison. -
Check
passwordResetCreatedAt+ TTL (passwordResetTokenTtlHours, default 1 hour). -
Enforce password strength (zxcvbn score ≥
minimumPasswordStrength). -
Update the user:
{ "$set": { "password": "<bcrypt hash>" }, "$unset": { "passwordResetToken": "", "passwordResetCreatedAt": "" } } -
Return
200 OK.
Error responses
| Status | Reason |
|---|---|
|
Missing fields; password too weak; token not found or expired |
|
Note
|
The token is one-shot: it is $unset on first successful use, so the link cannot be replayed.
|