Skip to content

fix(oauth): revoke replaced sessions and preserve token metadata - #1147

Open
EhabY wants to merge 1 commit into
mainfrom
fix/oauth-scope-session-lifecycle
Open

EhabY wants to merge 1 commit into
mainfrom
fix/oauth-scope-session-lifecycle

Conversation

@EhabY

@EhabY EhabY commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #1138's token-cleanup comment: clean up replaced OAuth sessions without forcing an immediate re-login when required scopes change. Closes #1140: this PR covers items 1 and 3, and #1128 covers item 2.

  • Keep existing expiry behavior: missing required scopes stop refresh but keep the stored access token, so normal 401 recovery applies after expiry. The eligibility check is renamed to canRefreshOAuthSession.
  • Revoke replaced sessions: a successful sign-in saves first, then revokes the overwritten OAuth pair in the background (best effort) with the client registration read before login. Failed or canceled sign-ins, same-token reuse, and stored-session adoption revoke nothing.
  • Preserve OAuth metadata on reuse: reusing the same token on the same origin keeps its refresh credentials and scopes. If a refresh rotates the session while its stored token is checked, the rotated session wins.
  • Share OAuth plumbing: revokeOAuthTokens in src/oauth/revocation.ts serves logout and replacement. withOAuthMetadata in src/oauth/metadataClient.ts creates the client and fetches metadata for revocation and token refresh.
  • Scopes: request user:update_personal to refresh expired workspace external-auth links. Inbox permissions stay optional.
  • Dynamic client registration disabled (OAuth follow-ups: DCR off by default on 2.38, unnoticed nightly scope failures, CLI inheriting CODER_SESSION_TOKEN #1140, item 1): when registration returns 403 (the default from Coder 2.38), a modal names dynamic_client_registration_enabled and offers Sign In with Token. Dismissing it cancels sign-in, so nothing switches methods silently.
  • CLI token from the environment (OAuth follow-ups: DCR off by default on 2.38, unnoticed nightly scope failures, CLI inheriting CODER_SESSION_TOKEN #1140, item 3): the shared global flags (getGlobalFlags and getGlobalShellFlags) now start with an empty --token=. The CLI applies flags over environment variables, so CODER_SESSION_TOKEN in the extension host no longer overrides the stored session. That holds for any case variant and for HOMEBREW_CODER_SESSION_TOKEN. Every invocation is covered, including the SSH ProxyCommand and terminals. A user's own --token in coder.globalFlags comes later and still wins. coder login --use-token-as-session omits the flag because it reads the token from the environment. It gets an environment with any host case variants of CODER_SESSION_TOKEN removed, so only the extension's token remains. This behavior was checked in the Coder source from v0.25.0 (the minimum supported version) through main.

Out of scope: startup or remote admission gates, per-action permission maps, and a background-refresh redesign. Before expiry, users can still see feature-specific permission errors; those don't prompt for sign-in.

Change size

Diff against the merge base; counts include moved code and renames.

Area Added Removed Net
Production (src/**) 260 149 +111
Tests (test/**) 266 34 +232
Changelog 12 3 +9
Total 538 186 +352

Validation

Single commit on top of #1134.

  • pnpm test: 189 files, 2,830 passed, 6 skipped.
  • pnpm typecheck, pnpm lint, and pnpm format:check: passed.
  • Integration tests were not rerun for this revision.
  • Regression tests cover stored and provided token reuse; manual, provided, and OAuth replacement; refresh rotation during a stored-token login; save-before-revoke ordering; failed and canceled sign-in; the registration 403 path (accept and dismiss); the empty --token= flag in every CLI invocation, a user --token overriding it, storeToken omitting it, and storeToken dropping a host coder_session_token.

Known limitations

  • A background refresh already in flight when an OAuth sign-in replaces the session can race the revocation. Fixing that needs refresh coordination, which is out of scope here.
  • If another window re-registers the client mid-login, revoking the old session can fail. The failure is logged at warn level.

Generated by Coder Agents on behalf of @EhabY.

@EhabY
EhabY force-pushed the fix/oauth-scope-session-lifecycle branch from e8d23c6 to 8125427 Compare October 9, 2026 23:05
@EhabY EhabY changed the title fix(oauth): preserve and revoke sessions across scope upgrades fix(oauth): revoke replaced sessions and preserve token metadata Oct 9, 2026
@EhabY
EhabY force-pushed the fix/oauth-scope-session-lifecycle branch 7 times, most recently from 72117ed to c01e269 Compare October 11, 2026 13:56
Clean up replaced OAuth sessions without forcing a re-login when the
required scopes change.

- Insufficient scopes prevent refresh but keep the stored access token, so
  normal 401 recovery applies after expiry. Rename the check to
  `canRefreshOAuthSession`.
- Save a successful replacement first, then revoke the overwritten OAuth
  pair in the background with the client registration captured before
  login. Failed or canceled logins, same-token reuse, and stored-session
  adoption revoke nothing.
- Keep refresh credentials and scopes when the same token is reused on the
  same origin, instead of turning it into a manual-token session. If a
  refresh rotates the session while its stored token is checked, keep the
  rotated session.
- Share revocation with logout through `withOAuthMetadata`, which token
  refresh also uses, and request `user:update_personal` to refresh expired
  external-auth links.
- If the server refuses dynamic client registration (off by default from
  Coder 2.38), name the setting and offer session-token sign-in.
- Start every CLI invocation's global flags with an empty `--token=`, so
  `CODER_SESSION_TOKEN` in the extension host (in any case) no longer
  overrides the stored session. This covers the SSH ProxyCommand and
  terminals too. `coder login --use-token-as-session` omits it, since it
  reads the token from the environment, and gets an environment without
  the host's case variants of the variable.

Closes #1140
@EhabY
EhabY force-pushed the fix/oauth-scope-session-lifecycle branch from c01e269 to 183b2bc Compare October 11, 2026 14:30

This branch has not been deployed

No deployments
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.

OAuth follow-ups: DCR off by default on 2.38, unnoticed nightly scope failures, CLI inheriting CODER_SESSION_TOKEN

1 participant