Repository navigation
Description
This was generated by AI during triage.
Context
From a security audit (2026-07-22, OWASP-checklist review). The anonymous auth endpoints have no throttling of any kind, and the configured Identity account lockout is separately known to be inert (#128) — so today there is zero brute-force defence on login. An attacker can spray passwords across accounts (rotating emails sidesteps per-user lockout even once #128 is fixed) or hammer the refresh endpoint.
Agent Brief
Category: enhancement
Summary: Add server-side rate limiting to the anonymous auth endpoints (login and refresh).
Current behavior:
POST /api/v1/auth/login and POST /api/v1/auth/refresh allow anonymous access with no rate limiting. Unlimited attempts per client, no throttling, no 429 responses anywhere in the API.
Desired behavior:
- A strict rate-limit policy applies to the auth endpoint group (e.g. on the order of 10 requests per 15-minute window per client — exact numbers are the implementer's call, but they should stop password spraying while never bothering a legitimate farm user who typoes a password a few times).
- Exceeding the limit returns
429with aRetry-Afterheader and a ProblemDetails body consistent with the API's other error responses. - The partition key is the client IP. Behind the reverse proxy this must be the original client IP (forwarded-headers handling — see the security-headers/forwarded-headers issue; if that hasn't landed yet, this issue must handle
X-Forwarded-Forfrom the trusted proxy itself or the limiter will throttle the proxy's IP, i.e. everyone at once). - Non-auth endpoints are unaffected.
Key interfaces:
- ASP.NET Core's built-in rate limiter (
AddRateLimiter+UseRateLimiter), with the policy attached to the auth endpoint group (RequireRateLimitingor group-level metadata) rather than individual endpoints, so future auth endpoints inherit it. - The limiter's rejection handler — must produce ProblemDetails, matching the API's existing error contract.
Acceptance criteria:
- Integration test: bursting the login endpoint past the limit returns
429withRetry-After; requests within the limit behave as before. - Integration test: same for the refresh endpoint.
- Integration test: a non-auth endpoint is not rate limited by this policy.
-
429body is ProblemDetails-shaped. - Limits are configurable via app configuration with safe defaults baked in.
Out of scope:
- Fixing the inert account lockout — tracked in Auth: configured account lockout is inert (CheckPasswordAsync never counts failures) #128 (complementary, per-user defence).
- CAPTCHA or proof-of-work challenges.
- Distributed rate limiting across multiple API replicas (deployment is single-instance; in-memory limiter is fine).
- Global/API-wide rate limiting.
Activity
F144: forwarded proto, HSTS, CSP + security headers, pinned AllowedHo…
F128: enforce the configured account lockout (CheckPasswordAsync neve…
F145 review: fix logout not revoking; harden cookie Secure; misc
F145: move refresh token to an HttpOnly cookie; access token in memor…
feat(api): accept browser error reports on /api/v1/client-errors (#217)
Observability: frontend error reporting — ErrorBoundary → API + trace…
test(api): address #260 review — assert human-readable control names;…
feat(api): fail Production boot when RateLimiting:TrustedProxies is e…
feat(auth): tell the operator when an instance has no admin yet (#363)
docs(jobs): sync deploy-invariant to the #271 leader gate
feat(jobs): single-runner leader gate for the durable job worker (#271)…
feat(ratelimit): distributed IP-keyed auth limiters (#544) (#558)
test(ci): isolate the login rate-limit bucket per test host (#876)