Skip to content

refactor(auth): delegate API-token validation to the OAuth provider - #443

Merged
mattzcarey merged 5 commits into
mainfrom
refactor/delegate-api-token-auth-to-provider
Aug 10, 2026
Merged

refactor(auth): delegate API-token validation to the OAuth provider#443
mattzcarey merged 5 commits into
mainfrom
refactor/delegate-api-token-auth-to-provider

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What

Replaces the hand-rolled bearer-token fork in front of OAuthProvider with the provider's resolveExternalToken hook, on @cloudflare/workers-oauth-provider 0.10.3 (was 0.8.2). This is the same architecture cloudflare/mcp adopted in cloudflare/mcp#185 and cloudflare/mcp#193.

  • packages/mcp-common/src/api-token-mode.ts: direct Cloudflare API/OAuth credentials are now resolved inside the provider via resolveExternalToken. Expected verification failures throw ExternalTokenError, which the provider converts to structured 401 invalid_token / 403 insufficient_scope (with WWW-Authenticate challenges and step-up scope guidance) / 429 temporarily_unavailable (preserving Retry-After) / 5xx server_error responses. Nothing in this path can escape as an uncaught Worker exception (HTTP 500 / error 1101) anymore.
  • Verified identities are cached in OAUTH_KV for 30 days, keyed by a SHA-256 digest of the credential, so repeat MCP requests skip the /user + /accounts identity probes (~500ms and 2 subrequests per request today). Failed verifications are never cached, and identities degraded by an ok-status-but-unparseable API payload are served but never cached, so transient API payload problems self-heal on the next probe. Revoking a credential still revokes access — tool calls use the token itself, not the cached identity.
  • The only remaining pre-provider branch is the local-development DEV_DISABLE_OAUTH short-circuit, which now returns structured errors for rejected dev tokens instead of throwing.
  • /oauth/authorize classifies the provider 0.10 AuthorizationError (redirect to the validated client redirect URI with error/state/iss, or render locally when no redirect URI was validated) and CimdFetchError (retryable 503) instead of collapsing them into a 500. /oauth/callback returns a client error when completeAuthorization rejects a grant reconstructed from an older provider version (relevant only during the deploy window).
  • Removes a vestigial createClient call in /oauth/callback that never registered the requesting client (the provider generates a random client ID) and only wrote an orphan KV record per authorization. Clients register through /register and are validated before the callback can run.
  • Provider 0.10.x also brings its own hardening to the fleet: OAuth 2.1 PKCE S256 enforcement for public clients, registered-capability validation, strict RFC 8707 resource handling, and RFC-compliant bare bearer challenges.

Why

A well-formed bearer token that fails verification (invalid credential, revoked token, malformed value) previously threw an MCPError that escaped the Worker as an exception, returning HTTP 500 / Cloudflare error 1101 to MCP clients instead of a spec-correct 401 with WWW-Authenticate. #393 converted most of these inside the fork; this PR removes the fork entirely so the provider owns the protected-resource error contract, and upgrades the fleet to the current provider.

Notes for review

  • Account-scoped tokens (no /user identity) remain supported: cfat_-prefixed tokens use only the /accounts probe and must resolve to exactly one account; legacy unprefixed tokens keep response-based inference.
  • Provider-issued tokens that miss the KV lookup (expired/revoked) now cost one identity-probe round-trip before returning invalid_token — same trade-off cloudflare/mcp accepted.
  • Legacy clients that authorize without PKCE, or with plain PKCE, are rejected by provider 0.10.x per OAuth 2.1; MCP-spec clients are unaffected. In-flight authorizations started under 0.8.2 without a code challenge fail as client errors during the deploy window.
  • The ai-gateway integration spec's implicit-flow helper now registers grantTypes: ['implicit'] / responseTypes: ['token'] because provider 0.10 validates registered client capabilities at completeAuthorization().

Testing

  • pnpm test: 318 tests green across 35 files, including new coverage for resolveExternalToken (identity caching, failed-verification and degraded-identity cache exclusion, KV-outage fallback, prefixed-owner probes, 400/401/403/429 mapping), /sse with a direct API token through the router, dev-mode structured errors, and the authorize-route error classification.
  • pnpm types, pnpm check:turbo, pnpm check:format, pnpm check:deps all green.

Upgrade workers-oauth-provider to 0.10.1 and replace the pre-provider
bearer-token fork with the provider's resolveExternalToken hook. Expected
verification failures now become structured 401/403/429 protected-resource
responses with WWW-Authenticate challenges instead of escaping as Worker
exceptions. Verified identities are cached against a credential digest so
repeat MCP requests skip the Cloudflare API identity probes, and
authorize-endpoint validation failures redirect to validated client
redirect URIs or render locally per OAuth 2.1.
…ration

devApiTokenModeEnabled inspects only the environment, so it no longer
reads as a per-request predicate. The OAuth callback's createClient call
never registered the requesting client: the provider generates a random
client ID, so the call only wrote an orphan KV record per authorization.
Clients register through the provider's /register endpoint, and the
authorize flow validates them before the callback can run.
…paths

getUserAndAccounts now reports when an ok-status probe returned an
unparseable payload; such identities may serve the current request but
are never written to the identity cache, so a transient Cloudflare API
payload problem self-heals on the next probe instead of pinning reduced
account data for the cache TTL. Local development mode converts expected
verification failures into structured responses again, and the OAuth
callback returns a client error when completeAuthorization rejects a
grant reconstructed from an older provider version.
@mattzcarey
mattzcarey marked this pull request as ready for review August 7, 2026 15:32
@mattzcarey
mattzcarey merged commit 12a9a61 into main Aug 10, 2026
6 checks passed
@mattzcarey
mattzcarey deleted the refactor/delegate-api-token-auth-to-provider branch August 10, 2026 17:54
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