mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
oauth: compare-and-swap session writes so a concurrent refresh cannot delete a live session
Refresh tokens rotate on use, and DoWithSession serializes refreshes per DID with an in-process mutex. That is the right mechanism and it protects nothing once there are two instances: both can refresh the same account at the same time, the slower one presents a refresh token the auth server has already superseded, gets invalid_grant, and isAuthError deletes the session. The user is signed out mid-push, and the session another instance had just legitimately refreshed is destroyed along with it. oauth_sessions gains a rev that increments on every write. A store that has read a session writes with a compare-and-swap against the revision it read and gets ErrSessionRevConflict if anyone wrote first, so a stale writer can no longer replace rotated tokens with invalidated ones. The persist callback treats that conflict as an ordinary outcome rather than an error, since leaving the newer state alone is exactly right. The delete path is now guarded by the same signal. An auth error on a session whose revision has moved since we read it means "someone else refreshed this", not "this session is dead", so it retries once against the newer tokens instead of deleting. Exactly once: a second failure means staleness was not the problem, and looping would hold the per-DID lock while getting the same answer. The guard is deliberately conservative. A store without revisions, no recorded revision, a failed lookup, a session that is simply gone: all answer "not advanced" and keep the previous delete-on-error behavior. Wrongly claiming a concurrent refresh would keep a genuinely dead session alive with no way out but waiting; wrongly missing one costs a re-login. The sentinel lives in pkg/auth/oauth rather than next to the SQLite store, because pkg/appview/db already imports pkg/auth/oauth and the other direction would be an import cycle. The db package re-exports it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
934e4a2a59
commit
e75b2e246b
@@ -94,6 +94,11 @@ CREATE TABLE IF NOT EXISTS tags (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_did_repo ON tags(did, repository);
|
||||
|
||||
-- rev increments on every successful write, so a writer that read revision N can
|
||||
-- tell whether anyone has written since. Refresh tokens rotate on use, and the
|
||||
-- per-DID mutex that serializes refreshes is in-process only, so without this a
|
||||
-- second instance's concurrent refresh looks identical to a dead session and
|
||||
-- gets it deleted out from under the user. See OAuthStore.SaveSession.
|
||||
CREATE TABLE IF NOT EXISTS oauth_sessions (
|
||||
session_key TEXT PRIMARY KEY,
|
||||
account_did TEXT NOT NULL,
|
||||
@@ -101,6 +106,7 @@ CREATE TABLE IF NOT EXISTS oauth_sessions (
|
||||
session_data TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
rev INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(account_did, session_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth_sessions_did ON oauth_sessions(account_did);
|
||||
|
||||
Reference in New Issue
Block a user