Skip to content

feat(oauth): mirror RFC 8707 resource into Auth0's audience param#1277

Closed
brosand wants to merge 1 commit into
RhysSullivan:mainfrom
brosand:auth0-audience-param
Closed

feat(oauth): mirror RFC 8707 resource into Auth0's audience param#1277
brosand wants to merge 1 commit into
RhysSullivan:mainfrom
brosand:auth0-audience-param

Conversation

@brosand

@brosand brosand commented Jul 3, 2026

Copy link
Copy Markdown

feat(oauth): mirror RFC 8707 resource into Auth0's audience param

Problem

Executor's OAuth helpers send the standards-based RFC 8707 Resource Indicator (resource) on the authorization request and on every token-endpoint grant, as the MCP Authorization spec requires. Auth0 does not implement RFC 8707: its endpoints silently ignore resource and instead select the target API (resource server) via a proprietary audience parameter, which Auth0 documents as its equivalent of the resource indicator and which takes the same value (the API identifier URI).

The practical consequence is that registered Auth0 OAuth clients cannot mint tokens through executor at all for the client_credentials grant. Observed empirically against a real Auth0 tenant:

HTTP 403 access_denied
"No audience parameter was provided, and no default audience has been configured"

Authorization-code grants "succeed" but fall back to an opaque token for the default (userinfo) audience, which resource servers cannot validate.

Approach

Add a small host-keyed quirk helper, providerAudienceParam(endpointUrl, resource), in packages/core/sdk/src/oauth-helpers.ts, following the exact pattern of the existing providerAuthorizeExtras Google quirk (offline access / consent prompt keyed off accounts.google.com). It returns the resource value as the audience to send when the endpoint hostname is auth0.com or ends with .auth0.com (case-insensitive, label-boundary suffix check so evilauth0.com does not match), and undefined otherwise.

The helper is applied at all four request-construction sites, so audience is sent alongside (not instead of) resource:

  • buildAuthorizationUrl (authorization request). Set before extraParams merge, so an explicit caller-provided audience still wins.
  • exchangeAuthorizationCode (code exchange)
  • exchangeClientCredentials (client_credentials, the grant that hard-fails today)
  • refreshAccessToken (refresh)

Why this shape

  • oauth-service.ts already threads the client's stored resource into every one of these helpers, so the fix needs no changes to the OAuth client model, tool schemas, or DB schema, and existing Auth0 clients start working with no reconfiguration.
  • It mirrors the established convention for provider-specific non-RFC behavior (host-keyed, documented quirk in oauth-helpers.ts) rather than introducing a new per-client setting for a value that is always identical to resource.
  • Sending both resource and audience is safe: Auth0 ignores resource, and non-Auth0 providers never receive audience.

Test coverage

New tests in packages/core/sdk/src/oauth-helpers.test.ts, following the suite's existing patterns:

  • Unit tests for providerAudienceParam: Auth0 tenant hosts (including regional <tenant>.<region>.auth0.com), case insensitivity, lookalike hosts (evilauth0.com, auth0.com.evil.com), unparseable URLs, and missing/empty resource.
  • buildAuthorizationUrl: Auth0 authorize host emits both resource and audience; non-Auth0 hosts and no-resource inputs emit no audience; explicit extraParams.audience overrides the auto-mapping.
  • Wire-level assertions for all three token grants. Auth0 token URLs cannot point at the loopback test fixture (the mapping keys off the *.auth0.com host), so these use the helpers' injected fetch to capture the POSTed form body and assert audience is present for https://tenant.us.auth0.com/oauth/token and absent for the local non-Auth0 endpoint.

vitest run src/oauth-helpers.test.ts in packages/core/sdk: 60 tests passed (14 new). oxfmt --check, oxlint --deny-warnings, and package typecheck clean on the touched files.

Caveats

  • Custom Auth0 domains (CNAME tenants) cannot be detected from the URL, so the auto-mapping does not fire for them. Those tenants can configure a default audience on the Auth0 side (API Authorization Settings), or an explicit audience can be passed via extraParams on the authorize request. If demand shows up, a per-client opt-in flag could be added later without disturbing this default.
  • The mapping only fires when a resource is configured for the client; there is no behavior change for clients without one.
  • Auth0 may return a token whose granted audience differs from the request only in normalization (Auth0 treats the API identifier as an exact string). This change forwards the configured resource verbatim, which matches what Auth0 expects.

Auth0 does not implement RFC 8707 Resource Indicators: its endpoints
select the target API via a proprietary audience parameter and silently
ignore resource. Without it, client_credentials against an Auth0 tenant
fails with 403 access_denied ("No audience parameter was provided, and
no default audience has been configured"), so registered Auth0 OAuth
clients cannot mint tokens at all.

Mirror the resource value into audience on the authorization request,
code exchange, client_credentials exchange, and refresh, whenever the
endpoint host is an Auth0 tenant domain (*.auth0.com), keyed off the
host exactly like the existing providerAuthorizeExtras Google quirk.
Non-Auth0 endpoints are unaffected; an explicit extraParams audience on
the authorize URL still wins. Custom Auth0 domains are not auto-detected
and should configure a default audience tenant-side.
@brosand brosand closed this Jul 3, 2026
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