AUTH4D-336 password lockout keyed on address and IP
POST /t/{tenantId}/password/sign-in and POST /t/{tenantId}/password/change
(AUTH4D-144 (repository file docs/api-spec/auth4d-144.md)) used to lock an address after 10 wrong attempts in 15 minutes,
counted on the address alone. Anyone who could open a hosted-login flow could keep a chosen player
locked out of password sign-in. The lockout is now counted per address and IP address, with a much
larger per-address ceiling as a backstop. Request and response shapes do not change.
Budgets
Every attempt spends these budgets in order, before any hashing. The first one that is spent
answers 429 {"error": "rate_limited"} with Retry-After in seconds, and the budgets after it are
not charged.
| Budget | Key | Limit |
|---|---|---|
| Per IP | tenant, client IP | 30 attempts an hour |
| Lockout | tenant, normalised address and IP | 10 attempts in 15 minutes |
| Address ceiling | tenant, normalised address | 100 attempts in 15 minutes |
The client IP is the one the API resolves for every request (clientIp, from the platform’s
connecting-IP header). Keys are HMACs under AUTH_PRIVACY_KEY, so no address or IP address reaches
the Durable Object. The values are PASSWORD_BUDGETS in apps/api/src/modules/password/index.ts.
Lifting a lockout
Both address budgets lift on their own when their window ends. A successful password sign-in, a
successful password change or a successful email-code reset (POST /t/{tenantId}/password/reset/confirm) also lifts them at once: it deletes the address ceiling and
the lockout for the IP address the request came from. Lockouts held by other IP addresses stay until
they lift on their own. The rate-limit store gains an optional resetBudget operation for this
(AtomicStateStore in packages/contracts, implemented by packages/storage-do).
Enumeration resistance
The address budgets are keyed on the address, not the account, so an unknown address, an address
with no password and an existing account get the same answers, in the same order, with the same
Retry-After. A lockout is lifted only after the password or the email code is proven, so lifting
reveals nothing that the successful response did not already reveal.
Source: docs/api-spec/auth4d-336.md