Skip to content

perf(auth): offload synchronous token checks and throttle usage writes - #1380

Merged
swjnxyf merged 3 commits into
yyjeqhc:mainfrom
skygunner:contrib/audit-t8-auth-blocking
Nov 7, 2026
Merged

swjnxyf merged 3 commits into
yyjeqhc:mainfrom
skygunner:contrib/audit-t8-auth-blocking

Conversation

@skygunner

Copy link
Copy Markdown
Contributor

Problem

The bearer verifier chain (PAT lookup, OAuth2 access-token check with client/user validation) runs entirely synchronous SQLite work inside the async request path — on this project's store that means one shared writer Mutex<Connection>. Every successful authentication also wrote last_used_at, so each authenticated request performed at least one write while holding that connection, stalling the async worker thread for the duration whenever the store was contended.

Found in a full code audit of v0.5.0. No authorization behavior depends on it: last_used_at is display/audit metadata (token-management listing); expiry and revocation are re-validated from their own columns on every authentication.

Change

  • authenticate() now runs the whole verifier chain in one spawn_blocking task. The token moves into the task and is dropped with it (never logged); every failure — including a lost or panicked blocking task — remains a rejection; no check is skipped. The MCP public-ingress check (authenticate_mcp) gets the same treatment. Verifier bodies become inherent synchronous functions; the async trait wrappers remain for existing callers/tests.
  • last_used_at writes now carry a 60-second floor, implemented as a single conditional UPDATE in the store (AND (last_used_at IS NULL OR last_used_at <= ts - 60)): no cache, no read-modify-write race, at most one write per credential per window. LAST_USED_UPDATE_FLOOR_SECS is a named constant documenting the contract.
  • Signatures take Arc<Config> so the chain can move onto the blocking task without cloning Config.

Validation

  • Structural regression test authenticate_database_work_runs_on_the_blocking_pool (current-thread runtime): with the store's writer mutex held from the runtime thread, authentication still completes. With the previous inline behavior this exact scenario deadlocks the runtime thread outright — the test's own deadline cannot even fire, which is the failure mode itself; the fixed code completes in ~0.1s.
  • Store test last_used_at_updates_are_floored_to_one_write_per_window: first observation writes, an update 30s later is skipped, past the 60s floor the value moves again — for api keys and OAuth access tokens.
  • The computer post-observation MCP test drove its call future exactly once and assumed admission had synchronously completed; it now drives the call until the action is admitted with the same exactly-one-request assertion (admission legitimately follows the async auth hop now).
  • cargo test --locked -p webcodex: 3,902 passed, 0 failed; cargo test --locked -p webcodex-store --lib: 324 passed, 0 failed; clippy -D clippy::correctness and cargo fmt --check clean.

Compatibility

No auth decisions, wire formats, or persisted schemas change. Error classification for purged/stale tokens is unchanged (all failures deny). Authentication now hops to the blocking pool, which adds a scheduling handoff per authentication — bounded by the existing blocking-pool size, and the win is removing writer-lock stalls from async workers.

vincent and others added 3 commits November 7, 2026 04:47
The bearer verifier chain (PAT lookup, OAuth2 access-token check with
client/user/ingress validation) runs entirely synchronous SQLite work inside
the async request path, and every successful authentication also wrote
last_used_at. On the shared single writer connection that stalls an async
worker whenever the store is contended and adds one write per request.

Run the whole chain on the blocking pool from one spawn_blocking in
authenticate() (and the MCP public-ingress check likewise): the token moves
into the task and is dropped with it, every failure — including a lost
blocking task — stays a rejection, and no check is skipped. last_used_at
writes now carry a 60-second floor implemented as a single conditional UPDATE
in the store (no cache, no read-modify-write race); the field is
display/audit metadata, and expiry/revocation are still validated on every
authentication.

The verifier bodies become inherent sync functions with the async trait
wrappers retained; signatures take Arc<Config> so the chain can move onto the
blocking task without cloning Config.

Regression proof: with the store's writer mutex held from the runtime thread,
authentication on a current-thread runtime still completes — the pre-fix
inline behavior deadlocks that runtime outright (the test's own deadline
cannot fire, which is the failure mode itself). The computer post-observation
MCP test drove its call future once and assumed admission had already
happened; it now drives the call until admission with the same exactly-one
assertion.
# Conflicts:
#	crates/webcodex-store/src/db_tests.rs

@swjnxyf swjnxyf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed blocking-pool PAT/OAuth2 authentication, fail-closed join/error paths, public MCP ingress fence, the conditional 60-second last_used_at write floor, and changed call sites. Integrated latest main, resolving the shared Store test insertion without dropping #1379's bounded purge regression. Fixed the three-platform production warnings by fencing obsolete async TokenVerifier adapters to tests in follow-up ddd90db. On wkp: 325 Store tests, 575 auth-related Server tests, and 13 Computer post-observation regressions all passed; cargo fmt/diff checks passed; production dogfood check completed with 0 warnings and Cargo exit 0. No blocking issues remain; approving for the maintainer-authorized squash merge without waiting for the updated-head hosted CI.

@swjnxyf
swjnxyf merged commit 04c8717 into yyjeqhc:main Nov 7, 2026
5 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.

3 participants