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>
2.6 KiB
2.6 KiB
Quickstart: Login Brute-Force Protection
Prerequisites
- API running (via
docker compose upor locally with.envset) curlavailable
Scenario 1: Trigger the rate limiter
Send 6 consecutive failed login attempts (default threshold is 5):
for i in $(seq 1 6); do
echo "Attempt $i:"
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "wrong", "password": "wrong"}'
done
Expected output:
Attempt 1: 401
Attempt 2: 401
Attempt 3: 401
Attempt 4: 401
Attempt 5: 401
Attempt 6: 429
The 6th attempt returns 429. Inspect the headers:
curl -i -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "wrong", "password": "wrong"}'
Expected headers include:
HTTP/1.1 429 Too Many Requests
Retry-After: 900
Expected body:
{"detail": "Too many failed login attempts. Please try again later.", "code": "login_rate_limited"}
Scenario 2: Successful login resets the counter
Make some failed attempts, then log in with valid credentials:
# Fail twice
for i in 1 2; do
curl -s -o /dev/null -w "fail $i: %{http_code}\n" \
-X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "wrong", "password": "wrong"}'
done
# Succeed — resets counter
curl -s -o /dev/null -w "success: %{http_code}\n" \
-X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "'"$OWNER_USERNAME"'", "password": "'"$OWNER_PASSWORD"'"}'
# Now fail 5 more times — counter was reset, so no 429 yet
for i in $(seq 1 5); do
curl -s -o /dev/null -w "fail after reset $i: %{http_code}\n" \
-X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "wrong", "password": "wrong"}'
done
Expected: all "fail after reset" lines return 401 (not 429), confirming the counter was reset.
Scenario 3: Observe log output
While triggering the rate limiter (Scenario 1), watch API logs:
docker compose logs -f api
After the threshold is crossed you should see a line like:
WARNING app.auth.rate_limiter:rate_limiter.py:NN Login blocked for 172.18.0.1 after 5 failures
Environment variable overrides
To test with a lower threshold without code changes:
LOGIN_MAX_FAILURES=2 LOGIN_WINDOW_SECONDS=60 LOGIN_COOLDOWN_SECONDS=30 \
uvicorn app.main:app --reload
Then only 2 failures trigger the lockout, and it clears after 30 seconds.