Auth Endpoints — Public Contract¶
Per-endpoint contract for the authentication surface — what each route does at the API edge, what rate-limit bucket it consumes, and what a 429 response looks like to a client. This is the public-facing contract. For the implementation (middleware, Redis keys, IP blocker, suspicious-activity heuristics), see Security → Rate Limiting. For the full per-route reference (request bodies, response shapes, error codes), see Swagger / OpenAPI. For the service-level breakdown (what each route delegates to), see Backend → Auth service.
Rate-limit buckets per endpoint¶
Each limit is per client IP and applies to the listed route. Counters reset on a sliding window backed by Redis.
| Endpoint | Limit | Window | Bucket |
|---|---|---|---|
POST /api/v1/auth/register |
5 | 1 hour | RegisterLimit() |
POST /api/v1/auth/login |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/google-login |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/line-login |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/apple-login |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/webauthn/login/begin |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/webauthn/login/finish |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/otp/send |
5 | 1 hour | RegisterLimit() |
POST /api/v1/auth/otp/verify |
10 | 15 minutes | LoginLimit() |
POST /api/v1/auth/reset-password |
5 | 1 hour | ResetPasswordLimit() |
Every route under /api/v1/* (baseline) |
600 | 1 minute | APILimit() |
Bucket presets are defined in backend/internal/middleware/rate_limiter.go.
How the buckets stack
APILimit() is wired on the /api/v1 group, so every API request counts against the 600/min/IP baseline. Targeted limits (register, login, reset-password) are layered on top of the baseline for those specific endpoints — a registration attempt counts against both the 5/hour register bucket and the 600/min baseline. Routes under /ws (WebSocket) and webhook endpoints are not in the baseline.
Counters are per-IP, not per-user
Multiple users behind a single NAT share a counter. A shared office network can exhaust the bucket for everyone if one person bursts on the login endpoint. The trade-off is intentional: rate limiting runs before authentication, so it has no user identity to key on.
Response when limited¶
HTTP/1.1 429 Too Many Requests
Retry-After: 900
Content-Type: application/json
{
"error": "too many login attempts, please try again later (max 10 per 15 minutes)"
}
The exact error message varies per bucket. The body uses the legacy { "error": ... } shape today — clients should treat any 429 as rate-limited regardless of body shape.
Retry-After¶
Retry-After: <seconds> is set to the window duration for the bucket that fired:
| Bucket | Retry-After |
|---|---|
APILimit() |
60 |
LoginLimit() |
900 |
RegisterLimit() |
3600 |
ResetPasswordLimit() |
3600 |
Clients should:
- Honour the header — wait at least that many seconds before retrying.
- Surface a generic "try again later" message to the user; showing the countdown is fine.
- Not retry the same request automatically. These limits exist to defeat scripted attacks, and silent retries defeat the purpose.
Escalation: 429 → 403 IP block¶
Repeated rate-limit violations escalate to an outright IP block. Once blocked, every subsequent request — including unrelated routes — returns 403 with error: IP_BLOCKED. See Security → Rate Limiting for:
- Auto-block threshold (5 violations within 1 hour → 24-hour block)
- Suspicious activity heuristics (SQL-injection patterns, path traversal → 48-hour block)
- How to unblock an IP
Failure mode¶
If Redis is unavailable, the rate limiter fails open — every request is allowed through. This is intentional: a Redis outage should not lock everyone out of the API. The trade-off is that during an outage an attacker has free reign on the auth endpoints; recover Redis quickly.
Authenticated routes¶
Routes behind JWTAuth aren't rate-limited per-route today. The defense layers are:
- The unauthenticated auth endpoints' rate limits keep brute-force off the doorstep.
- The global IP blocker catches repeated offenders regardless of route.
- Authenticated callers carry a
user_id— abuse on those routes is investigated and dealt with via admin action rather than per-IP throttling.
If a future authenticated endpoint needs per-user throttling, key the bucket on user_id from the JWT context rather than IP — wrappers for this live next to LoginLimit() in rate_limiter.go.