Edit Page

Password Reset

RESTHeart

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.

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 status is active:

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

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

      {baseAppUrl}/reset-password?email=alice@example.com&token=<passwordResetToken>
    • TTL: passwordResetTokenTtlHours (default 1 hour).

  4. Always return 202 Accepted — even if the email is unknown or the user is invited/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

  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. 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.