Skip to content

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

Open
EhabY wants to merge 2 commits into
mainfrom
fix/oauth-scope-session-lifecycle
Open

EhabY wants to merge 2 commits 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). Failed or canceled sign-ins, same-token reuse, and stored-session adoption revoke nothing.
  • Refresh and revoke as the issuing client: each OAuth session records the client ID and secret it was issued to, so refresh and revocation keep working when another window re-registers the client. Sessions saved before this change fall back to the stored registration, read before login can replace it.
  • Preserve OAuth metadata on reuse: reusing the same token on the same origin keeps its refresh credentials, scopes, and client. If a refresh rotates the session while its stored token is checked, the rotated session wins.
  • Keep credentials out of logs: a logged Axios error is reduced to its request line, status, and the server's reply, with credential fields redacted. Its config and socket hold the session token, OAuth tokens, and client secret, which the connection log buffer used to replay into the output channel.
  • 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, which coder start needs to refresh expired external-auth links. No narrower scope exists, so it also allows profile and Git SSH key changes; a comment next to the scope says so. 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. The changelog notes that OAuth becomes the default sign-in method and links the OAuth2 provider docs.
  • 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/**) 325 179 +146
Tests (test/**) 396 56 +340
Changelog 17 3 +14
Total 738 238 +500

Validation

Two commits on top of #1134.

  • pnpm test: 189 files, 2,834 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; refresh and revocation as the recorded client, with the stored registration as fallback; and an Axios error whose logged form omits its token, client secret, and session header.

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.
  • An Axios error nested in another error's cause is logged in full. No code wraps Axios errors that way today.

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
@matifali

Copy link
Copy Markdown
Member

The changelog for this PR also notes that OAuth will be the default auth method in the future. And add a link to the Coder docs.

…ient

- Log Axios errors as their request line and the server's reply. Their
  config and socket hold the session token, OAuth tokens, and client
  secret, which the buffered connection logs replayed into the output
  channel.
- Record the client that issued each OAuth session, and refresh and
  revoke as that client, so another window re-registering the client no
  longer breaks either. Sessions without a recorded client use the stored
  registration.
- Explain why the extension requests `user:update_personal`.
- In the changelog, note that OAuth becomes the default sign-in method
  and link the OAuth2 provider docs.
- Shorten comments.
@EhabY
EhabY requested a review from code-asher October 12, 2026 19:47

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

2 participants