After LOGIN_MAX_FAILURES consecutive failed attempts from the same source IP within LOGIN_WINDOW_SECONDS, POST /api/v1/auth/token returns HTTP 429 with a Retry-After header for LOGIN_COOLDOWN_SECONDS. A successful login resets the counter. Trusted upstream proxy IPs/CIDRs can be declared via LOGIN_TRUSTED_PROXY_IPS so X-Forwarded-For is honoured correctly behind nginx ingress or similar reverse proxies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
54 lines
2.6 KiB
Markdown
54 lines
2.6 KiB
Markdown
# Data Model: Login Brute-Force Protection
|
|
|
|
## Overview
|
|
|
|
This feature introduces no new database tables. The only data entity is a transient,
|
|
in-memory rate-limit record that does not survive process restarts. This is intentional
|
|
(see research.md Decision 3).
|
|
|
|
---
|
|
|
|
## Entity: Rate-Limit Record (in-memory only)
|
|
|
|
| Field | Type | Description |
|
|
|----------------|---------|-----------------------------------------------------------------------------|
|
|
| `failures` | int | Count of consecutive failed login attempts in the current window |
|
|
| `window_start` | float | Unix timestamp marking when the current counting window began |
|
|
| `blocked_until`| float | Unix timestamp after which the source is no longer blocked; 0.0 if not blocked |
|
|
|
|
**Keyed by**: resolved client IP address string (e.g., `"192.168.1.1"`); see `get_client_ip()` in `rate_limiter.py` for resolution logic
|
|
|
|
**Lifecycle**:
|
|
1. Record is created on the first failed login from a source.
|
|
2. `failures` increments on each subsequent failure within the window.
|
|
3. When `failures >= LOGIN_MAX_FAILURES`, `blocked_until` is set to `now + LOGIN_COOLDOWN_SECONDS`.
|
|
4. When `blocked_until` has passed, the record is deleted on the next request from that source.
|
|
5. A successful login deletes the record immediately (failure counter reset).
|
|
6. If `now - window_start > LOGIN_WINDOW_SECONDS` without triggering lockout, the counter resets within the existing record.
|
|
|
|
**State machine**:
|
|
|
|
```
|
|
[no record]
|
|
│ first failure
|
|
▼
|
|
[tracking] ──── failure N ≥ max ────► [blocked]
|
|
│ │
|
|
│ success / window expires │ cooldown expires
|
|
▼ ▼
|
|
[no record] ◄─────────────────────── [no record]
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Entity: Rate-Limit Settings
|
|
|
|
Stored as environment variables; loaded via `app.config.Settings`:
|
|
|
|
| Env Var | Default | Description |
|
|
|----------------------------|---------|----------------------------------------------------------|
|
|
| `LOGIN_MAX_FAILURES` | `5` | Failures within window before lockout |
|
|
| `LOGIN_WINDOW_SECONDS` | `300` | Rolling window duration in seconds (5 minutes) |
|
|
| `LOGIN_COOLDOWN_SECONDS` | `900` | Lockout duration in seconds after threshold exceeded (15 minutes) |
|
|
| `LOGIN_TRUSTED_PROXY_IPS` | `""` | Comma-separated IPs/CIDRs of trusted upstream proxies (e.g., `10.0.0.0/8`); empty = disabled |
|