Rate Limiting on Authentication Pages (SEC-14)
Rate Limiting on Authentication Pages (SEC-14)
As of v0.1.138, the /sign-in and /sign-up page routes are protected by the same auth rate limit tier that already covered the /api/auth/* API endpoints.
Background
The Auth.js API callbacks at /api/auth/* have been rate-limited since earlier releases (SEC-08): each IP address is allowed a maximum of 10 requests per 60-second sliding window. However, the browser-facing /sign-in and /sign-up pages themselves were classified as public routes and received no rate limiting. An attacker could use those pages to drive repeated form submissions — and therefore repeated calls to the underlying API callbacks — without being throttled at the page layer.
SEC-14 closes this gap by extending rate limiting to the page routes so that both layers share the same per-IP counter.
How it works
Shared rate limit tier
All of the following paths now map to RATE_LIMITS.auth:
| Path pattern | Type |
|---|---|
/api/auth/* | Auth.js callbacks, session, CSRF |
/sign-in and /sign-in/* | Sign-in page |
/sign-up and /sign-up/* | Sign-up / account creation page |
The 60-second sliding window is shared across all these paths per IP address. A burst of requests that hits both the page routes and the API routes will exhaust the same counter.
Limit: 10 requests / 60 seconds per IP address.
Response behaviour
The response when the limit is exceeded differs by route type:
| Route type | Rate limit response |
|---|---|
/api/* paths | HTTP 429 JSON body with Retry-After and X-RateLimit-* headers |
Page paths (/sign-in, /sign-up) | HTTP 302 redirect to the same page with ?error=rate_limited&retryAfter=N |
Browser-facing pages receive a redirect rather than a raw JSON 429 because browsers render the page response, not the API body. The redirect allows the page to display a human-readable error message.
User-facing error message
When /sign-in is loaded with ?error=rate_limited in the URL, an accessible alert banner is shown:
Too many sign-in attempts Please wait 60 seconds before trying again.
The banner uses role="alert" and aria-live="polite" so it is announced by screen readers.
The retryAfter value in the query parameter reflects the actual number of seconds remaining until the window resets for that IP.
Response headers
All rate-limited responses — whether a redirect or a JSON 429 — include the following headers:
| Header | Description |
|---|---|
Retry-After | Seconds until the rate limit window resets |
X-RateLimit-Limit | Maximum requests allowed in the window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Security posture
With this change, credential stuffing attacks against the sign-in and sign-up pages are throttled at the middleware layer — before any authentication logic or database query is executed — in the same way that OAuth token harvesting attacks against /api/auth/* have been throttled since SEC-08.
Security headers (CSP, Strict-Transport-Security, X-Content-Type-Options, etc.) are also applied to rate-limit redirect responses, not only to normal page responses.