Skip to content

Give each auth route its own rate-limit bucket, and release 0.17.2 - #45

Merged
mdfarhankc merged 2 commits into
mainfrom
fix/rate-limit-buckets
Sep 29, 2026
Merged

mdfarhankc merged 2 commits into
mainfrom
fix/rate-limit-buckets

Conversation

@mdfarhankc

Copy link
Copy Markdown
Owner

Two commits: the fixes, then the version bump. One merge instead of two, since the release contains nothing else.

The reported bug

Every Redis-backed limiter writes into one keyspace and the key was the client IP alone, so all six auth routes counted into a single sorted set and the smallest cap won for all of them. With register at 3, any three auth requests — a token refresh, a failed login — returned 429 on the next registration attempt for the rest of the window. It also undid the separate bucket reauth was given in 0.17.0.

Reproduced before changing anything:

redis : 3 hits on refresh (cap 30) -> [True, True, True]
redis : then register (cap 3, never used) -> False
memory: then register (cap 3, never used) -> True

The key now carries the route name. Namespacing at the caller rather than inside a backend keeps custom backends registered through register_rate_limiter_backend working unchanged, and keeps these buckets clear of any limiter an application passes to RateLimitMiddleware, which shares the same prefix.

Two more of the same kind

The last three bugs all came from the same place: the suite runs the memory backend, production auto-selects Redis whenever REDIS_URL is set. So I diffed the two backends across all four protection components on identical call sequences.

  • The Redis challenge store raised on a non-positive TTL where the in-memory one wrote an already-expired entry. PASSKEY_CHALLENGE_TTL=0 meant 500 from POST /passkeys/authenticate/begin, a route that needs no account. Both now store nothing and answer None.
  • No numeric setting had bounds. MAX_LOGIN_ATTEMPTS=0 locked every account on its first failed login; ACCESS_TOKEN_EXPIRE_MINUTES=0 issued dead tokens. Rejected at construction now, naming the setting. JWT_LEEWAY_SECONDS, PASSWORD_MAX_LENGTH and REAUTH_MAX_AGE_SECONDS keep their documented zero.

After the fixes the backends agree on every case checked.

Upgrade notes

  • Rate-limit counters reset once on deploy, because the key format changed.
  • A configuration holding one of the impossible values above will now refuse to start rather than misbehave later.

Checks

ruff, ruff format, mypy --strict, pytest (602 passed), mkdocs build --strict. The new guards are mutation-tested, and the rate-limit test fails on Redis and passes on memory when the fix is reverted, which is exactly why this escaped.

Every Redis-backed limiter writes into one keyspace, and the key was the
client IP alone, so all six auth routes counted into a single sorted set
and the smallest cap won for all of them. With register at 3, any three
auth requests, a token refresh or a failed login included, returned 429
on the next registration for the rest of the window. It also undid the
separate bucket reauth was given in 0.17.0. The key now carries the route
name. Existing counters reset once on deploy.

Namespacing at the caller rather than inside a backend keeps custom
backends working unchanged, and keeps these buckets clear of any limiter
an application passes to RateLimitMiddleware.

Auditing the two backends against each other for the same class of
problem turned up two more:

The Redis challenge store raised on a non-positive TTL where the
in-memory one simply wrote an already-expired entry, so
PASSKEY_CHALLENGE_TTL=0 answered 500 from a route that needs no account.
Both now store nothing and return None.

No numeric setting had bounds. MAX_LOGIN_ATTEMPTS=0 locked every account
on its first failed login; ACCESS_TOKEN_EXPIRE_MINUTES=0 issued dead
tokens. These are rejected at construction now. The three settings that
document a meaningful zero keep it.

The regression tests run against both backends, since the memory one
cannot show any of this.
Bumps the version and moves the Unreleased changelog entries under 0.17.2.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 73259f13-b30e-4dbd-85e3-f3d8b599a3ff


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.

@mdfarhankc
mdfarhankc merged commit d0fb017 into main Sep 29, 2026
7 checks passed
@mdfarhankc
mdfarhankc deleted the fix/rate-limit-buckets branch September 29, 2026 15:00
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.

1 participant