Skip to content

feat(share): keep OAuth identity across ordinary share restarts - #1337

Draft
skygunner wants to merge 1 commit into
yyjeqhc:mainfrom
skygunner:contrib/share-oauth-restart-persistence-20261028
Draft

skygunner wants to merge 1 commit into
yyjeqhc:mainfrom
skygunner:contrib/share-oauth-restart-persistence-20261028

Conversation

@skygunner

Copy link
Copy Markdown
Contributor

Draft: the mechanism is implemented and validated; the open question is the product default (see Design questions).

Problem

webcodex share --auth oauth persists its OAuth client ID/secret, but every start retires the previous run's grants and mints a fresh random session subject. An ordinary restart of the same share therefore forces the MCP client to reauthorize even when the persisted client, the grant audience and the grant lifetime are unchanged. Operators restart a share for many ordinary reasons (configuration, binary upgrade, host reboot), and every restart breaks an otherwise valid connection.

Change

An explicit, opt-in restart policy for OAuth shares:

  • --oauth-persist-on-restart (requires --auth oauth) keeps the validated persisted OAuth client and its unexpired, unrevoked grants across ordinary restarts of the same share.
  • The OAuth session subject is derived from the persisted client ID instead of a fresh random value, so the retained grants keep the same subject. Derivation accepts only the canonical generated client-ID form; anything else fails closed.
  • Startup skips the previous-run grant retirement only when the flag is set. Expiry, revocation, scope, resource/audience and token-replay rules still decide whether a retained token is usable; a restarted share replays no effects.

Default behavior without the flag is byte-for-byte the current behavior: persist the client, retire the previous run's grants, mint a new random session subject.

Compatibility

  • No schema or store migration: the flag reuses the existing protected client-ID file and the existing OAuth grant tables.
  • Enabling the flag for an existing share can require one reauthorization, because grants created by earlier runs were issued under a random session subject. After that the identity is stable.
  • Nothing about Runner transport, scopes, or project authority changes. OAuth access tokens are still rejected on Runner transport.

Validation

In-process regressions (Linux x86_64, Rust 1.99.1):

cargo test --locked -p webcodex --lib project_share_restart_preserves_client
  -> 1 passed; 3837 filtered out
cargo test --locked -p webcodex --lib persisted_client_session_is_stable
  -> 1 passed; 3837 filtered out
cargo test --locked -p webcodex --lib oauth_persist_on_restart_is_an_explicit
  -> 1 passed; 3837 filtered out
cargo test --locked -p webcodex --lib project_share
  -> 16 passed; 3822 filtered out (related suite; no regressions)
cargo fmt --all -- --check                                                             -> exit 0

project_share_restart_preserves_client_and_only_retires_default_mode_grants covers, against a real isolated SQLite store: client id/secret continuity across repeated persisting restarts, stable derived session subject, retained access and refresh grants, a revoked grant staying unusable after a persisting restart, and the default mode still retiring retained grants.

Real-process loopback E2E (scripts/e2e_share_oauth_restart.py): a real webcodex share process (real Server + WebSocket Runner, loopback, temporary state dir, real OAuth authorization-code + PKCE S256 with the share's own client) is authorized, stopped, restarted with the flag (client identity, access token and refresh rotation verified), then restarted without the flag to prove the default mode still invalidates the pre-restart grants.

cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner   -> exit 0
python3 scripts/e2e_share_oauth_restart.py --bin-dir target/dogfood                     -> exit 0
  PASS oauth restart persistence: client continuity, grant continuity, refresh rotation, default-mode revocation

The harness starts a real share, completes PKCE S256, checks an authorized MCP tools/list, stops only its own process group, restarts with the flag (same client id/secret, retained access token accepted, refresh rotation accepted, rotated token accepted), then restarts without the flag and requires 401 for the pre-restart access token and a refused refresh. Temporary state is removed after a passing run.

Not run here: Windows and macOS lanes (no environment available; the harness is POSIX-only like the existing share E2E). No deployment, service restart or production state is involved.

Design questions

  1. Should restart persistence be the default for OAuth shares? This PR keeps upstream behavior as the default and makes persistence explicit. A default-on policy is friendlier for connector use but changes the restart-isolation property for every existing OAuth share; say if you prefer that as a follow-up so we keep the mechanism and policy decision separable.
  2. Session identity source. Deriving the subject from the persisted client ID adds no new durable state, but it couples subject identity to the client record. If you would rather persist a separate session identity next to the client, that is a small, contained replacement.

An OAuth share persisted its client but retired grants and minted a fresh random session subject on every start, forcing reauthorization for an ordinary restart. Add an explicit --oauth-persist-on-restart opt-in that keeps the validated client and its unexpired, unrevoked grants via a session subject derived from the persisted client id. Expiry, revocation, scope and replay rules are unchanged and the default behaviour is identical.
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