Edit Page

Password Reset

RESTHeart Cloud

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.

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)
  alt user found and verified
    R->>M: generate + store reset token (TTL 1h)
    R->>E: password reset email
  else user found but not yet verified
    R->>M: generate + store new emailVerificationToken
    R->>E: verification email (re-sent)
  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
  M-->>R: OK
  R-->>C: 200 OK (JWT via cookie or body, per delivery)

POST /auth/forgot-password — publicly accessible ($unauthenticated).

Request

POST /auth/forgot-password
Content-Type: application/json

{ "email": "alice@example.com" }

Server-side steps

  1. Validate the email format.

  2. Look up the user by email (result is not revealed to the caller).

  3. If the user exists and is verified (roles ≠ ["$unauthenticated"]):

    • Generate a cryptographically random passwordResetToken (256-bit) and record passwordResetCreatedAt.

    • Send a reset email containing a one-time link:

      {frontendUrl}/auth/reset-password?email=alice@example.com&token=<passwordResetToken>
    • TTL: 1 hour.

  4. If the user exists but has not completed email verification yet (roles = ["$unauthenticated"]): there is no password to reset. Instead, the account’s original activation link is re-sent:

    • Generate a new emailVerificationToken, invalidating the previous one (recovers from an expired or lost original).

    • Send the same verification email used at registration time, containing:

      {frontendUrl}/auth/verify?email=alice@example.com&token=<emailVerificationToken>
    • This is the account owner’s only self-service way to recover a lost/expired activation link — registration itself rejects the email as already taken, so without this fallback the account would be stuck forever.

  5. Always return 202 Accepted — even if the email is unknown.

Note
Responding 202 unconditionally, and taking the same action regardless of which branch above ran, 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

  1. Validate all required fields.

  2. Find user by email.

  3. Compare passwordResetToken using constant-time comparison.

  4. Check passwordResetCreatedAt + TTL (passwordResetTokenTtlHours, default 1 hour).

  5. Enforce password strength (zxcvbn score ≥ minimumPasswordStrength).

  6. Update the user:

    {
      "$set":   { "password": "<bcrypt hash>" },
      "$unset": { "passwordResetToken": "", "passwordResetCreatedAt": "" }
    }
  7. Auto-login: issue a fresh JWT and deliver it per the optional delivery query parameter — cookie (default; sets the rh_auth cookie) or body (since 9.6.0; returns access_token/token_type/expires_in in the response body plus the Auth-Token header, no cookie).

  8. Return 200 OK.

Error responses

Status Reason

400

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.