Skip to content

auth: /oauth/token keeps a hand-maintained scope allowlist separate from AuthStandardClientScopes #5356

Description

@piero-dev25

What happened

Adding `AuthPresenceReadScope` (increment 1 of #92, "Set Up Integration") to `AuthStandardClientScopes` in `packages/contracts/src/auth.ts` silently broke the desktop bootstrap token exchange with `invalid_scope` (5 failing tests in `server.test.ts`'s "server router seam" suite: token exchange, websocket ticket issuance, pairing credential issuance, websocket rpc handshake via ticket, CORS headers on auth success).

Root cause

`apps/server/src/auth/http.ts`'s `/oauth/token` handler (around line 278) keeps its own hand-maintained `allowedScopes` `Set`, independent of `AuthStandardClientScopes`:

allowedScopes: new Set<AuthEnvironmentScope>([
  AuthOrchestrationReadScope,
  AuthOrchestrationOperateScope,
  AuthTerminalOperateScope,
  AuthReviewWriteScope,
  AuthAccessReadScope,
  AuthAccessWriteScope,
  AuthRelayReadScope,
  AuthRelayWriteScope,
  AuthPresenceCommandScope,
  AuthPresenceReadScope,
]),

This is a THIRD scope list in the codebase, distinct from both `AuthEnvironmentScope`'s own `Schema.Literals` union and `AuthStandardClientScopes` (which `AuthDesktopOwnerScopes` etc. derive from via spread). Nothing enforces that the three stay in sync. A caller requesting `AuthDesktopOwnerScopes.join(" ")` — which automatically picks up any new scope added to `AuthStandardClientScopes` — gets `invalid_scope` the moment a new scope exists in the derived list but not in this hand-copied one.

`AuthPresenceCommandScope` happening to already be present here (deliberately, per its own comment: requestable via token exchange but NOT auto-granted, since `presence:command` is desktop-owner-only) is what made increment 1's fix "just add it here too" easy this time — but that's exactly the trap: the next scope added to `AuthStandardClientScopes` won't have an obvious reason to also touch this unrelated file, and the failure mode is a same-shaped `invalid_scope` 400 with no compiler signal.

Suggested fix

Derive `allowedScopes` from `AuthStandardClientScopes` (plus whatever token-exchange-only scopes, like `AuthPresenceCommandScope`, are deliberately requestable-but-not-auto-granted) instead of hand-listing every entry — so adding a scope to the canonical list is sufficient, and a token-exchange-only addition is a single explicit union rather than a silent copy-paste site.

Where this was found

Found while building increment 1 of #92 ("Set Up Integration" detection probe) — adding the new `presence:read` scope required also fixing 3 hardcoded scope-list literals in test files (`EnvironmentAuth.test.ts`, `PairingGrantStore.test.ts`, `SessionStore.test.ts`) plus this one, real, production-code allowlist. Not fixed as part of that change beyond adding the new scope to this list — filing as its own ticket per team-lead's request, since the structural risk (three lists that must agree, no enforcement) outlives that one increment.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions