Repository navigation
Device login: rate limit the token endpoint per network and per device code - #830
Merged
Merged
Conversation
…ken endpoint A poll of a pending device code took a pool connection and then waited for the request's row lock (SELECT ... FOR UPDATE), so concurrent polls of one code, which anyone can send without an account, held up the whole pool and every database-backed request behind it. - lock_for_poll locks with FOR UPDATE NOWAIT. A poll that finds the row held (lock_not_available) answers slow_down at once, without recording a poll or waiting with a connection. - POST /auth/device/token is rate limited per instance before the database is asked: per client network, auth.device_polls_per_minute (600; 429 slow_down with Retry-After), and per device code, ten polls per ten seconds (slow_down). A KeyedWindowLimiter shares WindowLimiter's windows; a map full of live windows lets an untracked code through, so filling it cannot refuse everyone else's polls. The test sends 50 concurrent polls of one pending code to a server on a two-connection pool, also while another transaction holds the row: each is answered within a second, and an unrelated request is served. Fixes #819 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJZVgk6pyRx2Dk7AHqN
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Nov 3, 2026
main now locks a poll's request FOR UPDATE NOWAIT and answers slow_down when another poll holds it (#794, pull request #832), which this branch also did. The merge takes main's lock_for_poll, its Busy answer and its sized_pool test helper; this branch keeps the rate limits of the token endpoint per client network and per device code, which #794 does not add. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJZVgk6pyRx2Dk7AHqN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #819
Problem
Anyone can poll
POST /auth/device/tokenwithout an account, adevice_codecosts nothing to get, and the endpoint had no rate limit. A poll also used to wait for another poll's row lock while holding a pool connection, so concurrent polls of one pending code could fill the pool. Feeds, sign-ins and admin requests then failed with 503.#819 asks for two things:
device_code.The first is now on
main, from #794 (forgepkg/forge#832):lock_for_pollusesFOR UPDATE NOWAIT, and a poll that finds the row locked answersslow_down. This PR first had its ownNOWAITchange. The merge ofmaintook #794's version, so this PR now adds the second point and the issue's test.Change
DevicePollLimitsinauth/device.rs):auth.device_polls_per_minute, default 600,0turns it off. Past the limit a poll gets429slow_downwithRetry-After: 60, the same shape as/auth/device/code's limit. 600 a minute lets 10 logins poll at the shortest interval (1 s) from one address, or 50 at the default interval (5 s). The default is high so that installs where many users share one address, or where every client appears to come from the reverse proxy, keep working.400slow_down. The limit is looser than the database's owntoo_fastcheck, so a client that simply polls too fast is still slowed by that check, and its stored interval still grows by 5 s.KeyedWindowLimiter<K>(forge-core): the same fixed windows asWindowLimiter, now shared through a privateWindows<K>, with a key you choose. One difference: when the map is full of live windows,WindowLimiterrefuses a network it does not track yet, whileKeyedWindowLimiterlets an untracked key through without counting it. Otherwise anyone who filled the map with random codes would get every real login's polls refused. The network limit still applies to those polls. The code key is the first 64 bits of the code's SHA-256, so each entry has a fixed size.docs/deployment.md, the token endpoint's row in the endpoint table, the429sentence, and the key inforge.toml.example.Tests
crates/server/tests/device_auth.rsconcurrent_polls_of_a_pending_code_never_wait_for_its_row: the test the issue asks for. The server runs on a pool of 2 connections (sized_pool). It gets 50 concurrent polls of one pending code, each answeredauthorization_pendingorslow_downwithin a second; at least 40 of them are cut off by the per-code limit. An unrelated request (POST /auth/device/code) is served meanwhile. The same runs again while another transaction holds the row: all 50 polls getslow_down, and the stored interval stays unchanged.polls_are_limited_per_network: withdevice_polls_per_minute = 3, the fourth poll gets429withRetry-After: 60, so does a poll of an unknown code, and another network keeps polling.DevicePollLimits(both limits),code_key, andKeyedWindowLimiter(counting per key and per window, and a full map refusing new networks but letting new keys through).main: all 11device_authintegration tests (Device login: concurrent polls of an approved code hold every database connection, and the instance stops answering #794's included), the limiter and device unit tests,cargo fmt --check,cargo clippy -D warningson the changed crates, andcargo xtask changelog --check.Notes
docs/changes/2.2.0/649.feature.md, which now mentions the poll limits. It does not add aFixedentry for a bug no release had, which follows how Docker: challenge a request without credentials for an image a selector hides, so containerd and Kubernetes pulls send theirs #808 and Reader role migration: give administrators the reader role too, so one an identity provider demotes stays a reader #810 handled release-review fixes.[auth], likeoidc_login_starts_per_minute. The admin API is unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_012e6DJZVgk6pyRx2Dk7AHqN