Confirmed response shapes and behavior for Module 1 edge cases. Summary of what is implemented and what was not implemented for security reasons.
Everything Hari requested has been implemented in the backend unless noted otherwise in the “Not implemented (security)” section below.
| # | Requested behavior | Status |
|---|---|---|
| 1 | Social vs email collision: email/password on social-only → error; social on existing email → auto-link | Implemented |
| 2 | Forgot password for social-only: always 200, no email/token sent, same message | Implemented |
| 3 | Apple relay email stored as primary; two accounts if same person uses Google with real email; missing name accepted |
Implemented |
| 4 | Login error codes: WRONG_PASSWORD, ACCOUNT_NOT_FOUND, ACCOUNT_SOCIAL_ONLY, ACCOUNT_LOCKED, ACCOUNT_NOT_VERIFIED, TOO_MANY_ATTEMPTS; shape { error: { code, message, retry_after? } } |
Implemented |
| 5 | Rate limiting: 5 failed logins → account locked 15 min (ACCOUNT_LOCKED / TOO_MANY_ATTEMPTS + retry_after); forgot-password 3/hour per email (TOO_MANY_REQUESTS + retry_after) | Implemented |
| 6 | Email case-insensitive; backend normalizes to lowercase | Implemented |
| 7 | Access 15 min, refresh 7 days; multi-session; REFRESH_TOKEN_EXPIRED vs REFRESH_TOKEN_REVOKED | Implemented |
| 8 | Register returns tokens (auto-login); verification_pending documented for when verification is enabled | Implemented |
| 9 | Forgot-password always 200; reset token 1h, single use; reset does not return tokens | Implemented |
| 10 | SOCIAL_TOKEN_INVALID for invalid/expired idToken; PROVIDER_UNAVAILABLE (503) for provider down / testing with provider_unavailable |
Implemented |
| Item | Why not implemented |
|---|---|
| Revealing whether an email exists on login | We keep the same user-facing message for ACCOUNT_NOT_FOUND and WRONG_PASSWORD (“Invalid email or password”) so an attacker cannot enumerate accounts by distinguishing “account not found” from “wrong password”. Implementing different messages would weaken security. |
| Different success message for forgot-password when account is social-only | Returning a message like “You signed up with Google” would reveal account type. We always return the same generic message so we do not leak whether the email exists or how the account was created. |
error.code = "ACCOUNT_SOCIAL_ONLY".Always 200 with { "message": "If the email exists, a reset link has been sent." }. If the account is social-only we do not send email or create a token. Frontend shows the same generic message.
name accepted: existing user unchanged; new user gets email local part as name.retry_after (seconds).REQUIRE_EMAIL_VERIFICATION=true).retry_after (15 min in seconds).
retry_after. Scope: per email.TOO_MANY_REQUESTS and retry_after.Email is case-insensitive; backend normalizes to lowercase.
REFRESH_TOKEN_EXPIRED vs REFRESH_TOKEN_REVOKED.Register returns 201 with tokens (auto-login). When email verification is required, set REQUIRE_EMAIL_VERIFICATION=true; register can later return verification_pending and no tokens.
Always 200, same message. Reset token 1 hour, single use. After reset, only message (no tokens); user must log in.
SOCIAL_TOKEN_INVALID.PROVIDER_UNAVAILABLE. For testing, send idToken: "provider_unavailable" or "provider_error".
ACCOUNT_NOT_VERIFIED is active only when REQUIRE_EMAIL_VERIFICATION=true. Run migration 20260311120000_auth_lock_and_forgot_rate_limit for lock and forgot-password rate limit tables.