Skip to content

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:

  1. Honour the header — wait at least that many seconds before retrying.
  2. Surface a generic "try again later" message to the user; showing the countdown is fine.
  3. 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.