Problem
Grants created before the C-1a remediation (commit `256ba06`, 2026-04-10) have empty `scopes` inside their `encryptedProps` blob. When Claude Code's MCP client does a refresh-token exchange against one of these older grants, the new access token inherits the stale encrypted props — so `ctx.props.scopes` arrives at the gateway as `undefined` even though the grant's top-level `scope` field is correctly populated with `["generate", "read"]`.
The gateway treats this as "no scopes granted" and denies every scope-gated tool call. This silently locks out every user whose OAuth grant predates 2026-04-10.
Evidence (2026-04-11)
Dogfood session this morning:
- Kurt's admin grants: `grant:g5aL4ksbGjWkaG30tibfDrpsVgaVxE9Z:*` — authorizedAt 2026-03-24 through 2026-03-27, all have `scope: ["generate","read"]` at the top level ✓
- Kurt's personal grant: `grant:HWU9BjPldLkdBMchp2BQ8bnL8QrsXdD4:Wt9W21jbypueUS5e` — authorizedAt 2026-04-08, same shape ✓
- Every `mcp_session:*` record cached from those grants: `scopes: []` ✗
All four active sessions had to be manually patched in KV to restore the scopes and unblock MCP dogfooding. The patch has a 30-minute TTL; after that the sessions expire, `recoverSession` re-runs, `auth.scopes` re-resolves to `[]`, and we're back to square one.
Root cause
`src/gateway.ts` `resolveAuth` line 1014:
```ts
// C-1a remediation: thread through the actual scopes the OAuth token
// was issued with. Previously hardcoded to ['generate', 'read']...
scopes: oauthProps.scopes ?? [],
```
This only reads from `props.scopes` (the encrypted blob). It never falls back to the grant's top-level `scope` field, which is where the OAuth Provider library actually stores the authoritative scope list and which is always populated correctly regardless of whether the grant predates C-1a.
Proposed fix
When `oauthProps.scopes` is empty or missing but `oauthProps.userId` is set (meaning OAuth validation succeeded), look up the grant's top-level `scope` and use that. Options:
- Via OAuth Provider API: if `env.OAUTH_PROVIDER` exposes a `lookupGrant(grantId)` (or similar), read `grant.scope` from there. Ideal, but depends on library surface.
- Via direct KV read: thread `grantId` into `ctx.props` at `completeAuthorization` time, then read `grant::` from `OAUTH_KV` at request time. More brittle but works today.
- Backfill migration: run a one-shot script that reads every `grant:*` KV record, decrypts `encryptedProps`, injects `scopes: grant.scope` into the props blob, re-encrypts, writes back. One-time fix for existing grants; doesn't prevent the issue for any grants created between now and the code fix.
Option 3 is the cleanest one-shot remediation for the existing population. Option 1 or 2 is the code fix that prevents this from recurring.
Why this matters
The gateway's scope model is correct; the C-1a fix was correct. The bug is that legacy grants exist that the post-C-1a code can't read scopes from, and refresh-token exchange silently inherits the stale state forever. Without a fix, every pre-2026-04-10 OAuth user is broken — and they have no visible error that tells them why, because OAuth itself succeeds and the gateway only fails at tool call time.
Related
Problem
Grants created before the C-1a remediation (commit `256ba06`, 2026-04-10) have empty `scopes` inside their `encryptedProps` blob. When Claude Code's MCP client does a refresh-token exchange against one of these older grants, the new access token inherits the stale encrypted props — so `ctx.props.scopes` arrives at the gateway as `undefined` even though the grant's top-level `scope` field is correctly populated with `["generate", "read"]`.
The gateway treats this as "no scopes granted" and denies every scope-gated tool call. This silently locks out every user whose OAuth grant predates 2026-04-10.
Evidence (2026-04-11)
Dogfood session this morning:
All four active sessions had to be manually patched in KV to restore the scopes and unblock MCP dogfooding. The patch has a 30-minute TTL; after that the sessions expire, `recoverSession` re-runs, `auth.scopes` re-resolves to `[]`, and we're back to square one.
Root cause
`src/gateway.ts` `resolveAuth` line 1014:
```ts
// C-1a remediation: thread through the actual scopes the OAuth token
// was issued with. Previously hardcoded to ['generate', 'read']...
scopes: oauthProps.scopes ?? [],
```
This only reads from `props.scopes` (the encrypted blob). It never falls back to the grant's top-level `scope` field, which is where the OAuth Provider library actually stores the authoritative scope list and which is always populated correctly regardless of whether the grant predates C-1a.
Proposed fix
When `oauthProps.scopes` is empty or missing but `oauthProps.userId` is set (meaning OAuth validation succeeded), look up the grant's top-level `scope` and use that. Options:
Option 3 is the cleanest one-shot remediation for the existing population. Option 1 or 2 is the code fix that prevents this from recurring.
Why this matters
The gateway's scope model is correct; the C-1a fix was correct. The bug is that legacy grants exist that the post-C-1a code can't read scopes from, and refresh-token exchange silently inherits the stale state forever. Without a fix, every pre-2026-04-10 OAuth user is broken — and they have no visible error that tells them why, because OAuth itself succeeds and the gateway only fails at tool call time.
Related