feat(platform): login rate limiting with exponential backoff and lockout#1542
Conversation
Implements issue #1501. Protects sign-in against per-account brute force and distributed password-spray, records lockout events to the audit log and in-app notifications, and exposes an admin settings UI + logs view. - Better Auth hooks.before gates /sign-in/email on per-IP flood + per-account lockout state, returning max(accountLockout, ipLimit) as retry-after so the user sees the real unlock time. jitter masks the timing channel. - hooks.after bumps the failure counter on real wrong-password attempts only; lockedUntil never advances on blocked retries. Blocked attempts go to a coalesced hourly loginBlockCounters table instead of audit logs to keep the audit table bounded under DDoS. - login_policy governance type (Zod): maxAttemptsBeforeLockout, backoffSchedule, trustedProxies (admin-configurable). Defaults [1s,10s,60s,10min] after 5 failures. - Client IP extraction via proxy-addr, walking X-Forwarded-For right-to- left with trusted-proxy CIDRs. Defaults loopback+uniquelocal; admins can widen via Login policy editor. Defends against spoofed leftmost entries. - Audit log additions: login_attempt (failure, with lockedUntil in metadata once past threshold), login_lockout (first threshold crossing), login_success (NIST/SOC2 compliance). - Notifications: new notifications table + in-app bell in the sidebar, i18n-keyed titles/bodies, relative-time display, expand-to-view with explicit "Mark as read". - Audit log details dialog formats timestamp-suffix metadata fields (at/until/time/timestamp) via the existing useFormatDate. - Settings UI: Security & Monitoring group split into sub-tabs (Login & auth / PII / Usage). Login policy editor with trusted-proxy field; Save button dirty-tracks. - Logs page: new "Sign-in blocks" tab (admin-only) shows hourly counter rows filtered to org members. - Login form shows a specific "locked, try again in X" message on 429, with seconds/minutes/hours pluralization in en + de. - Better Auth's built-in rateLimit disabled — our gate owns all sign-in throttling. - Tests: delayForFailureCount, selectStrictestPolicy, parseLoginPolicy, client_ip with IPv4/IPv6/spoof/CIDR cases (21 tests, all green).
The login form's Better Auth onError callback had a required ctx parameter, but existing tests (and Better Auth itself under certain conditions) invoke it with no argument. Make ctx optional so the callback safely handles both call styles.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis pull request implements login attempt rate limiting with exponential backoff and account lockout functionality. The changes introduce login failure tracking (consecutive failure counts, lockout state), per-email and per-IP throttling via Better Auth middleware hooks, configurable lockout policies (max attempts and backoff schedules) via governance settings, hourly block counters for admin visibility, and a notification system for lockout events. New frontend components include a NotificationBell in the navigation, a LoginPolicyEditor for configuring attempt limits, and a BlockCountersTable for reviewing rejected sign-in attempts. Backend enhancements include trusted proxy IP detection, internal queries and mutations for failure recording, governance policy validation, and audit log/notification writes on lockout. Database schemas for login attempts, counters, and notifications are introduced alongside internationalization strings for lockout messaging and policy configuration. Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms Comment |
Closes #1501.
Summary
/sign-in/emailbehind a per-IP flood limit and per-account exponential lockout (default 1s → 10s → 60s → 10min after 5 failures). Blocked retries never advancelockedUntil, so the displayed countdown reflects the real unlock time.auditLogsinto an hourly-bucketedloginBlockCounterstable — keeps the audit table bounded even under 1M-req DDoS (150 audit rows vs 1M previously).proxy-addr: walksX-Forwarded-Forright-to-left skipping trusted-proxy CIDRs, defends against spoofed leftmost entries.at/until/time/timestamp) to local time via existinguseFormatDate.rateLimitexplicitly disabled — our gate owns all sign-in throttling.login_successaudit event.Tabs & components added
Schema additions
loginAttempts— per-email failure counter +lockedUntilloginBlockCounters— hourly(email, windowStart)coalesced blocked-attempt countsnotifications— org-scoped, i18n keys + params,readByarrayKnown gaps (documented, deferred)
/request-password-resetand/send-verification-emailnot yet gated (separate issue)Test plan
npx tsc --noEmitcleanbunx oxlint --type-awareclean (0 errors, 0 warnings)vitest run21/21 pass (backoff math, strictest-policy, policy parsing, IP parsing + spoof defense)loginBlockCounterswithout advancinglockedUntilSign-in blockstab renders counter rowsSummary by CodeRabbit
Release Notes
New Features
Messaging & Localization