Skip to main content

Password reset

The public password-reset flow is a two-step exchange: the visitor requests a reset for their email (createResetPasswordRequest), receives a token by mail, and submits the token together with a new password (resetPassword). An optional middle step (checkResetPasswordToken) lets the client validate the token before showing the form.

All three endpoints are gated by the PERSON_RESET_PASSWORD permission, normally held by the public login token.

Requesting a reset

mutation {
createResetPasswordRequest(
email: "[email protected]",
captchaToken: "0.aXR…",
options: { mailVariant: "en_us", mailProject: "my-blog" }
) {
ok
error { code retryAfter }
}
}
ArgNotes
emailThe address to reset.
captchaTokenRequired when captcha is configured. (since 2.2)
options.mailVariantOverride the mail template variant — see mail templates.
options.mailProjectOverride the project slug used to resolve project-specific templates.

Errors:

CodeCause
PERSON_NOT_FOUNDNo person with that email. Only returned when login.revealUserExists: true. When revealUserExists: false, the endpoint returns ok: true with no mail sent.
INVALID_CAPTCHACaptcha token missing or rejected. (since 2.2)
RATE_LIMIT_EXCEEDEDPer-IP rate limit (rateLimits.passwordResetPerIp) hit. (since 2.2) retryAfter carries the wait in seconds.

Per-email mail throttle (since 2.2)

In addition to the per-IP limit, each email address is subject to exponential backoff reusing the login.baseBackoff / login.maxBackoff / login.attemptWindow knobs. Repeated requests for the same email do not error — they just silently stop sending mail until the next allowed attempt. A successful resetPassword clears the counter.

This means a hostile request cannot flood the user's inbox even when the per-IP rate limit is generous.

Checking the token before showing the form

The reset link in the mail carries requestId and token. The client can verify the token is still valid before rendering a password input:

query {
checkResetPasswordToken(requestId: "…", token: "…")
}

Returns one of:

  • REQUEST_NOT_FOUNDrequestId does not exist
  • TOKEN_NOT_FOUNDtoken does not match the request
  • TOKEN_INVALID — token hash mismatch
  • TOKEN_USED — token has already been consumed
  • TOKEN_EXPIRED — token is past its expiration window

Anything other than one of those codes means the token is valid.

Completing the reset

mutation {
resetPassword(token: "…", password: "the new password") {
ok
error {
code
weakPasswordReasons
developerMessage
}
}
}

The password is validated against the active password policy including the optional HIBP check.

Errors:

CodeCause
TOKEN_NOT_FOUND, TOKEN_INVALID, TOKEN_USED, TOKEN_EXPIREDToken failed validation.
PASSWORD_TOO_WEAKPassword failed strength checks. weakPasswordReasons[] carries the reasons including COMPROMISED (since 2.2).

Successful reset:

  • Invalidates the reset token.
  • Resets the per-email reset throttle counter.
  • Records password_reset in the audit log.
  • Does not sign the user in. The client should follow up with signIn (or signInPasswordless / signInIDP).

Audit

EventWhen
password_reset_initcreateResetPasswordRequest, success or failure
password_resetresetPassword, success or failure