fix(rate-limit): exempt register-challenge from the per-IP floor (stateless mint) #441

Merged
nrupard merged 1 commit from fix/BUNYIP-450-exempt-register-challenge-rate-floor into main 2026-08-03 16:26:00 +02:00
Owner

BUNYIP-450 (relates BUNYIP-445 / BUNYIP-449). The durable fix behind the E2E register-challenge 429s.

Problem

GET /v1/auth/register-challenge sits under the per-IP API_UNAUTH rate-limit floor (20 req/60s/IP) because it is not in rate_limit_floor::EXEMPT_PATHS. But its handler mints only a stateless HS256 timestamp token (JwtService::create_signup_challenge_token, crates/bunyip-domain/src/services/jwt.rs:260 - no DB write, no side effect) and documents "needs no rate limit". So the floor adds no protection there; it only produces a false 429 for any real clients that share a source IP (a NAT/proxy) and for the E2E runner's single egress IP - which failed the disposable-account specs (BUNYIP-445 run #2453, mitigated harness-side in BUNYIP-449).

Change

Add /v1/auth/register-challenge to EXEMPT_PATHS. The register POST it precedes (POST /v1/auth/register) stays under the floor and the BUNYIP-377 bot guard, so removing the floor from the challenge mint alone opens no registration-abuse path.

Tests: the exempt-list test now asserts register-challenge is exempt; the capped-list test asserts /v1/auth/register (POST) stays floored and that the /v1/auth/register-challenge/extra near-miss does not fall through the exact-match list.

Verification

  • just check-container (fmt + clippy -D warnings + workspace lib tests) passes.

🤖 Generated with Claude Code

BUNYIP-450 (relates BUNYIP-445 / BUNYIP-449). The durable fix behind the E2E register-challenge 429s. ## Problem `GET /v1/auth/register-challenge` sits under the per-IP `API_UNAUTH` rate-limit floor (20 req/60s/IP) because it is not in `rate_limit_floor::EXEMPT_PATHS`. But its handler mints only a stateless HS256 timestamp token (`JwtService::create_signup_challenge_token`, `crates/bunyip-domain/src/services/jwt.rs:260` - no DB write, no side effect) and documents "needs no rate limit". So the floor adds no protection there; it only produces a false 429 for any real clients that share a source IP (a NAT/proxy) and for the E2E runner's single egress IP - which failed the disposable-account specs (BUNYIP-445 run #2453, mitigated harness-side in BUNYIP-449). ## Change Add `/v1/auth/register-challenge` to `EXEMPT_PATHS`. The register POST it precedes (`POST /v1/auth/register`) stays under the floor and the BUNYIP-377 bot guard, so removing the floor from the challenge mint alone opens no registration-abuse path. Tests: the exempt-list test now asserts register-challenge is exempt; the capped-list test asserts `/v1/auth/register` (POST) stays floored and that the `/v1/auth/register-challenge/extra` near-miss does not fall through the exact-match list. ## Verification - `just check-container` (fmt + clippy -D warnings + workspace lib tests) passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(rate-limit): exempt register-challenge from the per-IP floor
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 14s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m12s
Create release / Create release from merged PR (pull_request) Has been skipped
af4b0f9c54
GET /v1/auth/register-challenge sat under the API_UNAUTH floor (20 req/60s/IP) because it was not in rate_limit_floor::EXEMPT_PATHS, but its handler mints only a stateless HS256 timestamp token (JwtService::create_signup_challenge_token: no DB write, no side effect) and documents that it needs no rate limit. The floor added no protection there and only produced a false 429 for real clients sharing a source IP (a NAT/proxy) and for the E2E runner's single egress IP, which failed the disposable-account specs (BUNYIP-445, mitigated harness-side in BUNYIP-449). Add the path to EXEMPT_PATHS; the register POST it precedes stays floored and bot-guarded (BUNYIP-377). Tests assert the challenge is exempt, register POST stays capped, and the near-miss path does not fall through.

#BUNYIP-450

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard scheduled this pull request to auto merge when all checks succeed 2026-08-03 16:25:48 +02:00
nrupard deleted branch fix/BUNYIP-450-exempt-register-challenge-rate-floor 2026-08-03 16:26:00 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!441
No description provided.