Sign-up, OAuth & Invitations
Password Reset
The 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.
The same two steps also recover an account that never completed email verification: the reset link lets the owner set a password of their own and verifies the address at the same time (since 9.13.0).
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
participant E as Email
Note over C,E: Step 1 — Request reset link
C->>R: POST /auth/forgot-password {email}
R->>M: lookup user (result not revealed)
opt user found (verified or not)
R->>M: generate + store reset token (TTL 1h)
R->>E: password reset email
end
R-->>C: 202 Accepted (always)
Note over C,M: Step 2 — Apply new password
C->>R: PATCH /auth/reset-password {email, token, password}
Note over R: constant-time token compare + TTL check
Note over R: zxcvbn password strength check
R->>M: hash + save password, unset reset token
opt account not verified yet
R->>M: set roles → default-role, unset verification token
end
M-->>R: OK
R-->>C: 200 OK (JWT via cookie or body, per delivery)
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 an email was sent to this address less than
resend-cooldown-secondsago (default 300, see accountsEmailPolicy), stop here and send nothing. The last sending time is the more recent ofpasswordResetCreatedAtandemailVerificationCreatedAt, so the verification email sent at registration counts too. Since 9.12.2: before, every call sent an email. -
If the user exists, verified or not:
-
Generate a cryptographically random
passwordResetToken(256-bit) and recordpasswordResetCreatedAt. -
Send a reset email containing a one-time link:
{frontendUrl}/auth/reset-password?email=alice@example.com&token=<passwordResetToken> -
TTL: 1 hour.
-
-
Always return
202 Accepted— even if the email is unknown.
An account that has not completed email verification yet (roles = ["$unauthenticated"]) gets the same reset link. Following it proves ownership of the address, as the verification link does, and the reset makes the owner choose the password. This is also the owner’s only self-service way to recover a lost or expired verification email, since registration rejects the address as already taken.
Before 9.13.0 the verification link was re-sent instead. That link activated the account with the password chosen at registration, and since anyone can register any address, that password could have been chosen by someone else (GHSA-m7m7-gjxg-cjmm).
|
Note
|
Responding 202 unconditionally, also when the account does not exist or the cooldown stops the email, prevents an attacker from enumerating registered email addresses or their verification status 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": "" } } -
If the account had not completed email verification (roles =
["$unauthenticated"]), verify it now:$setrolesto the configureddefault-roleand$unsetemailVerificationTokenandemailVerificationCreatedAt, asGET /auth/verifydoes. Since 9.13.0: before, the reset was refused with400 Account not verified. -
Auto-login: issue a fresh JWT and deliver it per the optional
deliveryquery parameter —cookie(default; sets therh_authcookie) orbody(since 9.6.0; returnsaccess_token/token_type/expires_inin the response body plus theAuth-Tokenheader, no cookie). -
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.
|