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 }
}
}
| Arg | Notes |
|---|---|
email | The address to reset. |
captchaToken | Required when captcha is configured. (since 2.2) |
options.mailVariant | Override the mail template variant — see mail templates. |
options.mailProject | Override the project slug used to resolve project-specific templates. |
Errors:
| Code | Cause |
|---|---|
PERSON_NOT_FOUND | No person with that email. Only returned when login.revealUserExists: true. When revealUserExists: false, the endpoint returns ok: true with no mail sent. |
INVALID_CAPTCHA | Captcha token missing or rejected. (since 2.2) |
RATE_LIMIT_EXCEEDED | Per-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_FOUND—requestIddoes not existTOKEN_NOT_FOUND—tokendoes not match the requestTOKEN_INVALID— token hash mismatchTOKEN_USED— token has already been consumedTOKEN_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:
| Code | Cause |
|---|---|
TOKEN_NOT_FOUND, TOKEN_INVALID, TOKEN_USED, TOKEN_EXPIRED | Token failed validation. |
PASSWORD_TOO_WEAK | Password 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_resetin the audit log. - Does not sign the user in. The client should follow up with
signIn(orsignInPasswordless/signInIDP).
Audit
| Event | When |
|---|---|
password_reset_init | createResetPasswordRequest, success or failure |
password_reset | resetPassword, success or failure |