Skip to content

Validate OAuth-issued provider tokens carry API scope before onboarding completes - #574

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-5710-validate-oauth-issued-provider-tokens-carry-real-api-scope
Aug 23, 2026
Merged

Validate OAuth-issued provider tokens carry API scope before onboarding completes#574
TheGreatAxios merged 2 commits into
mainfrom
cl-5710-validate-oauth-issued-provider-tokens-carry-real-api-scope

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

Closes CL-5710.

Problem

The OAuth submit path (src/tui/provider-setup-submit.ts) treated onboarding as complete on any successful Codex/xAI login. A completed OAuth login proves the token is real (issued by the provider's own authorization server via a PKCE round-trip), but not that the token carries usable API scope — e.g. a chat-only subscription without API access. That gap surfaced as a confusing first-send auth error with no setup-attributable hint, the same failure shape CL-5687/5688/5689/5690 fixed for the API-key path (PR #405).

Confirmed still present on main: the oauth branch in buildProviderSubmitHandler persisted defaultProvider and the local selection unconditionally, with a comment explicitly noting the scope gap was "tracked separately."

Fix

Added src/auth/oauth-scope-check.ts, which probes the provider's own catalog/list endpoint with the token that was just issued — the same authoritative surface a real inference call would hit — rather than trusting any scope string the issuer echoes back:

  • Codex: GET /codex/models on chatgpt.com/backend-api with the same auth headers fetchCodexModels/fetchCodexUsage already use.
  • xAI: GET /models on the CLI chat proxy (cli-chat-proxy.grok.com/v1) with the same auth headers fetchXaiUsage already uses.

The result is classified into three states, and buildProviderSubmitHandler's oauth branch only blocks on one of them:

  • ok — proceed, onboarding completes normally.
  • insufficient-scope (a definitive 401/403) — the submit is rejected with an actionable message telling the user their sign-in lacks API access and to reconnect, instead of a raw adapter error. Nothing is persisted.
  • unavailable (network error, timeout, 429/5xx, or anything else inconclusive) — onboarding proceeds. A transient failure must never lock a legitimate user out of onboarding; only a proven scope failure blocks.

skipValidation (the existing Ctrl+S bypass) skips the probe entirely, matching the API-key path's behavior.

Neither the token nor any response body is logged or persisted — only the HTTP status is inspected.

Tests

  • src/auth/oauth-scope-check.test.ts — codex/xai probes: valid scope (200), definitive insufficient scope (401/403), and check-unavailable (network throw, timeout, 500).
  • src/tui/provider-setup-submit.test.ts — new describe block covering the same three paths through buildProviderSubmitHandler's oauth branch, plus a skipValidation bypass case.

bunx tsc --noEmit is clean; affected suites (src/auth/**, src/tui/provider-setup-submit.test.ts, src/tui/provider-setup.test.ts, src/tui/runner.test.ts) pass.

…ng completes

A completed Codex/xAI OAuth login proves the token is real but not that it
carries usable API scope (e.g. a chat-only subscription). Probe each
provider's own catalog endpoint with the issued token before treating
onboarding as complete: a definitive 401/403 rejects the submit with an
actionable message pointing back to reconnecting; a check that can't run at
all (network blip, timeout, 5xx) never blocks onboarding, only a proven
scope failure does. Neither the token nor any response body is logged or
persisted.
@linear-code

linear-code Bot commented Aug 23, 2026

Copy link
Copy Markdown

CL-5710

mock.module for codex/session.js and xai/session.js in
oauth-scope-check.test.ts, and for oauth-scope-check.js in
provider-setup-submit.test.ts, replaced those modules for the whole
bun test process without restoring them, breaking
tests/unit/codex-session.test.ts which imports the real module
directly. Capture the real module before mocking and restore it in
afterAll. Also drops an unused beforeEach import that eslint flagged.
@TheGreatAxios
TheGreatAxios merged commit 555867f into main Aug 23, 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.

1 participant