Accept a public forgot-password request and always return a non-disclosing success response while conditionally issuing a one-time reset token and sending the transactional email when the account exists.
- Public GraphQL mutation (
password_reset_request) exposed throughUserResolver. - No authenticated session required.
- Abuse protections required: honeypot, CAPTCHA verification, anti-abuse rate limiting.
- Input honeypot must remain empty.
- CAPTCHA token must be valid and non-replayed.
- Public source key must pass anti-abuse rate limit.
- Response contract must be identical whether the email exists or not.
- Reject request when honeypot is filled.
- Verify CAPTCHA token via
VerifyCaptchaTokenUsecase. - Check anti-abuse limiter using the source key (IP-based).
- Resolve user by submitted email.
- If user exists:
- generate unique
jti, - sign reset JWT (60 minutes) with minimal claims + UX fields,
- store
jtiin local reset-token cache with aligned TTL, - build URL
${fo_url}/reset?jwt=<token>, - resolve email template language from user country (
frfor FR-like locales, otherwise defaulten), - send
FitdeskForgotPasswordemail through Morgans service.
- generate unique
- Return
{ ok: true, messageKey: 'password_reset.request.sent_if_exists' }.
| Error | Cause |
|---|---|
PASSWORD_RESET_REQUEST_HONEYPOT_TRIGGERED |
Honeypot field is not empty |
INVALID_CAPTCHA_TOKEN |
CAPTCHA verification failed |
PASSWORD_RESET_REQUEST_RATE_LIMITED |
Source exceeded anti-abuse limit |
REQUEST_PASSWORD_RESET_USECASE |
Unexpected failure (fallback) |
- Non-disclosing behavior: clients cannot infer account existence from the success payload.
- Replay mitigation starts at issuance time by storing
jtiin process-local cache. - Reset tokens are intentionally volatile across API restarts (accepted single-instance tradeoff).