OAuth Protected Resource Plugin
The OAuthProtectedResourcePlugin allows you to configure your Zuplo gateway to
support OAuth protected resources through the
.well-known/oauth-protected-resource endpoint. See
RFC9728 for more details.
This is particularly useful when building an MCP Server on Zuplo. See the MCP Server Handler docs for more details.
Usage
This runtime plugin registers the .well-known/oauth-protected-resource route
on your behalf, along with every path beneath it, such as
/.well-known/oauth-protected-resource/mcp. If you configure an
OAuth Policy on a route with the
oAuthResourceMetadataEnabled option set to true, the policy answers requests
that carry no bearer token with a 401 response and a WWW-Authenticate header.
The header's resource_metadata parameter is the URL of the
.well-known/oauth-protected-resource endpoint for that route. When you set
scopesSupported, the header also carries a scope parameter that lists those
scopes.
Code
As per the MCP OAuth specification, you must use the canonical URL of your
authorization server as the authorizationServers value. The resourceName is
a human readable name for the resource.
Note that the .well-known/oauth-protected-resource endpoint explicitly has a
CORS policy of anything-goes since this is a public endpoint that should be
accessible to anyone to check the server's OAuth configuration.
Options
Construct the plugin inside runtimeInit. The plugin validates its options when
you construct it, so an invalid value fails at startup with a
ConfigurationError instead of failing a client's first login.
| Option | Type | Description |
|---|---|---|
authorizationServers | string[] | Canonical issuer URLs of the authorization servers that issue tokens for this resource. Each should comply with RFC 8414. Emitted as authorization_servers. |
resourceName | string | Human-readable name of the resource, intended for display to end users. RFC 9728 recommends setting it. Emitted as resource_name. |
scopesSupported | string[] | Scopes that clients should request when they obtain an access token for this resource. Emitted as scopes_supported, and as the scope parameter of the 401 WWW-Authenticate header by OAuth policies that have oAuthResourceMetadataEnabled set. Each entry must be one OAuth scope token (RFC 6749 section 3.3): printable ASCII with no spaces, double quotes, or backslashes. Use one entry per scope, never a space-delimited string. An empty array is rejected; omit the option to advertise no scopes. |
Advertising supported scopes
An MCP client has to decide which scopes to request from the authorization server. The MCP authorization specification tells clients to consider these sources in order:
- The
scopeparameter of theWWW-Authenticatechallenge on the 401 response. Clients treat these scopes as authoritative. - The
scopes_supportedarray in the protected resource metadata document, when the challenge has noscopeparameter. - The
scopes_supportedarray in the authorization server's own metadata. The specification doesn't list this source, but some clients fall back to it when the first two are empty. It usually contains every scope the server knows about. - No
scopeparameter at all.
Setting scopesSupported fills the first two sources with the same list, so no
client reaches the third. For example, a plugin configured with an Okta
authorization server, resourceName: "Acme MCP", and
scopesSupported: ["mcp:access", "offline_access"] serves this document at
/.well-known/oauth-protected-resource/mcp:
Code
A request to /mcp without a bearer token receives:
Code
If you don't set scopesSupported, the document has no scopes_supported key
and the header has no scope parameter. Requests that carry an invalid or
expired token receive a 401 without the WWW-Authenticate header in either
case.
Okta rejects its own default scopes
Okta custom authorization servers always publish device_sso and
interclient_access in their scopes_supported metadata. The Include in
public metadata toggle for those two scopes is disabled, so you can't remove
them. A client that falls back to that list requests them together with your
scopes, and Okta rejects the authorization request with
illegal_scope_combination and grants no scopes. Set scopesSupported whenever
Okta is your authorization server.