# Research: Login Brute-Force Protection ## Decision 1: Library vs. custom implementation **Decision**: Custom in-memory failure tracker (no new library dependency) **Rationale**: The requirement is to count *failed* login attempts specifically and reset on success — not to rate-limit all requests regardless of outcome. Popular libraries like `slowapi` count all requests to a route, which would break FR-004 (reset on success) without significant workarounds. A purpose-built 60-line class is simpler, more auditable, and has no dependency footprint. **Alternatives considered**: - `slowapi` (built on `limits`): Counts all requests, not failures. Requires patching the exception handler to decrement on success — fragile and non-obvious. - `slowapi` with a custom key function: Could be done, but the library's storage model doesn't expose a "reset this key" API in a clean way. - Redis-backed counter: Overkill for a single-user personal app with one instance. No new infrastructure justified. --- ## Decision 2: Fixed window vs. sliding window **Decision**: Fixed window with per-source reset on successful login **Rationale**: Fixed window is simpler to implement correctly and sufficient for this use case. The main attack — rapid sequential guessing — is fully addressed. The known "burst at window boundary" weakness is irrelevant here because: (a) the cooldown period is separate from the counting window, and (b) a successful login resets the counter entirely. **Alternatives considered**: - Sliding window: More accurate, but adds complexity (requires storing timestamps of each request). The marginal security benefit doesn't justify the implementation cost for a personal single-user app. --- ## Decision 3: In-memory backing store **Decision**: Python `dict` keyed by source IP, protected by a threading `Lock` **Rationale**: The application runs as a single process. In-memory storage means counters reset on restart — this is acceptable and matches the "fail open" assumption in the spec. No new infrastructure (Redis, database table) is required. **Alternatives considered**: - Database-backed counters: Persistent across restarts, but adds a DB round-trip to every login request (including successful ones). Not justified. - Redis: Distributed-safe and persistent, but requires a new service dependency. Out of scope for a personal single-instance app. --- ## Decision 4: Source identifier **Decision**: `request.client.host` (the TCP peer address) **Rationale**: The spec explicitly states not to trust `X-Forwarded-For` headers unless the app is known to be behind a trusted proxy. `request.client.host` in Starlette/FastAPI is the actual TCP peer IP — it cannot be spoofed by an attacker sending arbitrary headers. **Alternatives considered**: - `X-Forwarded-For` first value: Spoofable if the app is not behind a trusted proxy (attacker can set arbitrary header values). - `X-Real-IP`: Same spoofing concern. --- ## Decision 5: 429 response and Retry-After header **Decision**: Return HTTP 429 with `{"detail": "...", "code": "login_rate_limited"}` and a `Retry-After` header set to the configured cooldown duration in seconds **Rationale**: HTTP 429 is the standard "Too Many Requests" status. The `Retry-After` header is explicitly mentioned in the spec (US2 acceptance scenario) and is required by RFC 6585 for rate-limit responses. Setting it to the *configured* cooldown (not the exact remaining time) satisfies FR-005: it doesn't reveal precise expiry, just the maximum wait. The response body follows §3.3 of the constitution (error envelope with `detail` and `code`). --- ## Decision 6: Default threshold values **Decision**: `LOGIN_MAX_FAILURES=5`, `LOGIN_WINDOW_SECONDS=300` (5 min), `LOGIN_COOLDOWN_SECONDS=900` (15 min) **Rationale**: Industry standard for web apps. 5 attempts is enough for legitimate typos but makes brute-force infeasible at human scale. A 5-minute counting window matches typical "I fat-fingered my password" retry patterns. A 15-minute cooldown is a meaningful deterrent without locking out a legitimate owner indefinitely. **Alternatives considered**: - 3 failures / 60 s window / 300 s cooldown: More aggressive, but too likely to lock out the legitimate owner on a bad day. - 10 failures: Too permissive for a brute-force defense.