feat(portal): rate-limit and lock out brute-force portal logins (PMS-501) #385
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-501-portal-login-rate-limit-lockout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The portal login endpoint (
POST /api/v1/portal/auth/login) had no per-IP or per-account rate limiting, no failed-attempt accounting, and no lockout, so an attacker who knew a tenant slug and a contact email could grind unlimited password guesses bounded only by Argon2 latency.Add a layered in-memory limiter (
PortalLoginLimiter, mirroringauth::rate_limit::LoginLimiter) stored inPortalRouterStateand checked inline at the top of theloginhandler: 20/min per source IP plus 5/min per(tenant_slug, lowercased email)pair (the slug is part of the key because portal emails are only unique within a tenant). Over-quota returns 429 with aRetry-Afterheader.Add a persistent failed-attempt counter and exponential-backoff lockout that survives a process restart and spans replicas (which the in-memory store cannot): migration 082 adds
portal_failed_login_countandportal_locked_untiltocontacts. The service rejects with 429 while a lockout window is active, increments the counter on each credential failure, arms a doubling lock (30s, 60s, 120s, ... capped at 1h) once five consecutive failures accumulate, and clears both on the next successful login.Covered by unit tests for the backoff schedule and limiter keying, plus integration tests asserting the durable lockout state and the success-path counter reset.
#PMS-501