Skip to content

feat(auth): rate-limit dashboard sign-in against brute-force attempts - #437

Merged
tbille merged 3 commits into
mozilla-ai:mainfrom
champ18ion:feat/rate-limit-dashboard-signin
Jul 28, 2026
Merged

feat(auth): rate-limit dashboard sign-in against brute-force attempts#437
tbille merged 3 commits into
mozilla-ai:mainfrom
champ18ion:feat/rate-limit-dashboard-signin

Conversation

@champ18ion

@champ18ion champ18ion commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a per-IP throttle to POST /v1/auth/session, which had no rate limiting despite being a purpose-built credential-checking endpoint (each attempt touches the DB for a hash lookup). Not a new exposure, header auth on every management endpoint was already unthrottled, but this endpoint deserves its own purpose-built limiter.

Reuses the existing RateLimiter (sliding window, 429 + Retry-After) keyed by client IP instead of user_id, as a separate instance from the general rate_limit_rpm (which is keyed to authenticated users and never sees this pre-auth path). Only counts failed attempts, so a correct master key is never throttled.

Client IP comes from request.client.host, not a hand-parsed X-Forwarded-For: uvicorn's ProxyHeadersMiddleware already rewrites it from that header, but only when the immediate peer is trusted (loopback by default), the same trust boundary request_is_https already relies on for X-Forwarded-Proto. Parsing the header directly in application code would let anyone bypass the throttle by sending their own X-Forwarded-For.

New config: dashboard_login_rate_limit_per_minute (default 10, None disables). Surfaced read-only in the settings dashboard alongside rate_limit_rpm, not hot-settable, matching that field's own precedent.

PR Type

  • New Feature

Relevant issues

Fixes #390

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test) — note: ran via a manual pip venv, not uv, since Windows is excluded from your lockfile's supported platforms; integration tests (tests/integration) couldn't run locally without Docker, but this change is fully covered by tests/unit/test_dashboard_session.py (TestClient + SQLite), which passes.
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec — not applicable, no route signature changed.

AI Usage

  • AI was used for drafting/refactoring.

AI Model/Tool used: Claude Code (Sonnet 5)

Any additional AI details you'd like to share: Approach followed the plan you laid out in the issue; code written by Claude Code under my direction and review.

  • I am an AI Agent filling out this form

Summary

Added an optional, per-IP “failed login” rate limit for POST /v1/auth/session to help slow down brute-force attempts against the dashboard sign-in flow.

  • Introduced dashboard_login_rate_limit_per_minute (default 10; set to None to disable).
  • The limit applies only after a failed master-key verification; successful sign-ins are never throttled.
  • Once exceeded, the endpoint returns 429 with a Retry-After header and logs the throttled source IP.
  • The limiter is separate from the existing authenticated-user rate_limit_rpm.
  • Wired into app startup and exposed as a read-only setting in the settings dashboard.
  • Added unit tests for throttling on repeated failures, no throttling on success, disabling via config, and isolating rate-limit counters between different client IPs.
  • Updated Postman/OpenAPI documentation for the session endpoint’s master-key verification and the point at which throttling occurs.

Technical notes

  • Uses request.client.host (from trusted proxy handling) as the per-IP key.
  • The dashboard login limiter is stored on app state as login_rate_limiter.

@champ18ion
champ18ion temporarily deployed to integration-tests July 27, 2026 06:32 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ac71138-2703-4058-846d-090466a7dd8b

📥 Commits

Reviewing files that changed from the base of the PR and between f9b5c79 and 3c2a467.

⛔ Files ignored due to path filters (1)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
📒 Files selected for processing (6)
  • docs/public/otari.postman_collection.json
  • src/gateway/api/routes/auth_session.py
  • src/gateway/api/routes/settings.py
  • src/gateway/core/config.py
  • src/gateway/main.py
  • tests/unit/test_dashboard_session.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • docs/public/otari.postman_collection.json
  • src/gateway/main.py
  • src/gateway/core/config.py
  • src/gateway/api/routes/settings.py
  • src/gateway/api/routes/auth_session.py
  • tests/unit/test_dashboard_session.py

Walkthrough

Changes

The dashboard session endpoint now applies configurable per-IP throttling to failed master-key attempts. Application wiring, settings display, documentation, and tests cover enabled, successful-login, isolated-IP, and disabled-limit behavior.

Dashboard login throttling

Layer / File(s) Summary
Limiter configuration and app wiring
src/gateway/core/config.py, src/gateway/main.py, src/gateway/api/routes/settings.py
Adds the validated rate-limit setting, initializes its limiter, and exposes the effective value in settings.
Failed sign-in enforcement
src/gateway/api/routes/auth_session.py, docs/public/otari.postman_collection.json
Checks failed session attempts by client IP, propagates 429 responses, and documents verification and throttling order.
Login throttling validation
tests/unit/test_dashboard_session.py
Tests throttling thresholds, successful sign-in bypass, per-IP isolation, and disabled throttling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • mozilla-ai/otari#384: Introduced the session endpoint whose failed verification flow is extended here.

Suggested reviewers: njbrake

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is relevant, but it uses feat(auth): instead of the required Conventional Commit prefix feat:. Change it to start with feat: and keep the rest of the summary, e.g. feat: rate-limit dashboard sign-in against brute-force attempts.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description matches the template with all required sections present and the linked issue, checklist, and AI usage filled in.
Linked Issues check ✅ Passed The changes implement the per-IP login limiter, failed-only counting, 429/Retry-After behavior, trusted client IP handling, config, and tests requested by #390.
Out of Scope Changes check ✅ Passed The added docs, settings, config, route, and tests all support the login-rate-limit feature; no unrelated changes are evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from njbrake July 27, 2026 06:33
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/gateway/api/routes/auth_session.py 92.85% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/gateway/api/routes/settings.py 94.55% <ø> (ø)
src/gateway/core/config.py 95.26% <100.00%> (ø)
src/gateway/main.py 87.57% <100.00%> (ø)
src/gateway/api/routes/auth_session.py 90.90% <92.85%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/test_dashboard_session.py (1)

247-257: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cover isolation between client IPs.

All requests use one TestClient, so these assertions do not prove that separate IPs receive separate buckets. Add a test using two distinct client addresses; otherwise a regression to a global limiter could pass unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_dashboard_session.py` around lines 247 - 257, Extend
test_repeated_failed_sign_ins_get_throttled to issue failed sign-in requests
from two distinct client IP addresses, verifying each address has its own
rate-limit bucket. Preserve the existing per-IP throttling assertions while
ensuring a global limiter regression would fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gateway/api/routes/auth_session.py`:
- Around line 86-88: Update the authentication flow around is_valid_master_key
to run a non-consuming rate-limit admission check before credential
verification, rejecting over-limit requests before the database/hash lookup.
After verification fails, record the failed attempt and retain the existing
rate-limit handling; successful sign-ins must not consume rate-limit quota.

---

Nitpick comments:
In `@tests/unit/test_dashboard_session.py`:
- Around line 247-257: Extend test_repeated_failed_sign_ins_get_throttled to
issue failed sign-in requests from two distinct client IP addresses, verifying
each address has its own rate-limit bucket. Preserve the existing per-IP
throttling assertions while ensuring a global limiter regression would fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b181941f-5bb5-4780-8bfd-5de05ab8e490

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa80d0 and 53c5ee7.

📒 Files selected for processing (5)
  • src/gateway/api/routes/auth_session.py
  • src/gateway/api/routes/settings.py
  • src/gateway/core/config.py
  • src/gateway/main.py
  • tests/unit/test_dashboard_session.py

Comment thread src/gateway/api/routes/auth_session.py
champ18ion added a commit to champ18ion/otari that referenced this pull request Jul 27, 2026
CodeRabbit's review on mozilla-ai#437 suggested gating the DB/hash lookup in
is_valid_master_key behind a non-consuming rate-limit check before
verification, to stop an already-throttled IP from forcing that work
on every request.

Tried it, and a test caught a real regression: a pre-verification
gate can't know whether a given attempt would have succeeded, so once
an IP hits its failure quota, it also blocks that IP's legitimate
owner from signing in with the *correct* key, exactly what the issue
explicitly said must never happen. Reverted to checking only after a
failed verification (functionally unchanged from the original
commit), and documented why: the DB/hash lookup this exposes to
repeated attempts only runs on the auto-generated bootstrap-key path;
with a configured master_key, verification is a constant-time string
compare, not a DB round trip.
@champ18ion
champ18ion temporarily deployed to integration-tests July 27, 2026 15:09 — with GitHub Actions Inactive
champ18ion added a commit to champ18ion/otari that referenced this pull request Jul 27, 2026
Two things from mozilla-ai#437:

- The docstring added in 5cffd02 changed create_session's OpenAPI
  description, which wasn't regenerated at the time, failing the
  openapi-spec CI check. Regenerated both artifacts.
- CodeRabbit nitpick: all prior rate-limit tests used one TestClient,
  so none of them actually proved isolation between client IPs, only
  that throttling happens at all. A regression to a single global
  bucket would have passed unnoticed. Added a test using two
  TestClients on the same app with distinct client addresses,
  confirming one IP's throttling doesn't affect the other.
@champ18ion
champ18ion temporarily deployed to integration-tests July 27, 2026 15:25 — with GitHub Actions Inactive
@champ18ion

Copy link
Copy Markdown
Contributor Author

also fixed the openapi-spec CI failure (forgot to regenerate after adding docstrings in the last commit) and addressed the isolation nitpick (there wasn't a discrete thread for it, so replying here): added test_rate_limit_buckets_are_isolated_per_client_ip using two TestClients with distinct client addresses on the same app, confirms one IP's throttling doesn't bleed into another's bucket. both in f9b5c79

Adds a per-IP throttle to POST /v1/auth/session, which had no rate
limiting despite being a purpose-built credential-checking endpoint
(each attempt touches the DB for a hash lookup). Not a new exposure -
header auth on every management endpoint was already unthrottled -
but this endpoint deserves its own purpose-built limiter.

Reuses the existing RateLimiter (sliding window, 429 + Retry-After)
keyed by client IP instead of user_id, as a separate instance from
the general rate_limit_rpm (which is keyed to authenticated users and
never sees this pre-auth path). Only counts failed attempts, so a
correct master key is never throttled.

Client IP comes from request.client.host, not a hand-parsed
X-Forwarded-For: uvicorn's ProxyHeadersMiddleware already rewrites it
from that header, but only when the immediate peer is trusted
(loopback by default), the same trust boundary request_is_https
already relies on for X-Forwarded-Proto. Parsing the header directly
in application code would let anyone bypass the throttle by sending
their own X-Forwarded-For.

New config: dashboard_login_rate_limit_per_minute (default 10,
None disables). Surfaced read-only in the settings dashboard
alongside rate_limit_rpm, not hot-settable, matching that field's
own precedent.
CodeRabbit's review on mozilla-ai#437 suggested gating the DB/hash lookup in
is_valid_master_key behind a non-consuming rate-limit check before
verification, to stop an already-throttled IP from forcing that work
on every request.

Tried it, and a test caught a real regression: a pre-verification
gate can't know whether a given attempt would have succeeded, so once
an IP hits its failure quota, it also blocks that IP's legitimate
owner from signing in with the *correct* key, exactly what the issue
explicitly said must never happen. Reverted to checking only after a
failed verification (functionally unchanged from the original
commit), and documented why: the DB/hash lookup this exposes to
repeated attempts only runs on the auto-generated bootstrap-key path;
with a configured master_key, verification is a constant-time string
compare, not a DB round trip.
Two things from mozilla-ai#437:

- The docstring added in 5cffd02 changed create_session's OpenAPI
  description, which wasn't regenerated at the time, failing the
  openapi-spec CI check. Regenerated both artifacts.
- CodeRabbit nitpick: all prior rate-limit tests used one TestClient,
  so none of them actually proved isolation between client IPs, only
  that throttling happens at all. A regression to a single global
  bucket would have passed unnoticed. Added a test using two
  TestClients on the same app with distinct client addresses,
  confirming one IP's throttling doesn't affect the other.
@tbille
tbille force-pushed the feat/rate-limit-dashboard-signin branch from f9b5c79 to 3c2a467 Compare July 28, 2026 07:23
@tbille
tbille temporarily deployed to integration-tests July 28, 2026 07:23 — with GitHub Actions Inactive
@tbille

tbille commented Jul 28, 2026

Copy link
Copy Markdown
Member

This looks good! Thanks for the contribution @champ18ion 🎉

@tbille
tbille merged commit 2657bc9 into mozilla-ai:main Jul 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate-limit dashboard sign-in (POST /v1/auth/session)

3 participants