ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Getting Started
    Develop in the portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
API Management
AI Gateway
MCP Gateway
MCP Server
Developer Portal
Development
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
    Overview
    Request & Context
    Configuration
    Caching APIs
    Data Management
    Extensions & Hooks
    Error Handling
    Logging & Observability
    Types and Interfaces
    Web Standards
    MCP
    Advanced Topics
      Node ModulesCode ReuseRoute Custom DataClone Request/ResponseRuntime Behaviorszp-body-removedZuplo Identity TokenJWT Service PluginOAuth Protected Resource Plugin
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Advanced Topics

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
import { RuntimeExtensions, OAuthProtectedResourcePlugin, } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin( new OAuthProtectedResourcePlugin({ authorizationServers: ["https://your-auth0-domain.us.auth0.com"], resourceName: "My MCP OAuth Resource", scopesSupported: ["mcp:access", "offline_access"], }), ); }

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.

OptionTypeDescription
authorizationServersstring[]Canonical issuer URLs of the authorization servers that issue tokens for this resource. Each should comply with RFC 8414. Emitted as authorization_servers.
resourceNamestringHuman-readable name of the resource, intended for display to end users. RFC 9728 recommends setting it. Emitted as resource_name.
scopesSupportedstring[]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:

  1. The scope parameter of the WWW-Authenticate challenge on the 401 response. Clients treat these scopes as authoritative.
  2. The scopes_supported array in the protected resource metadata document, when the challenge has no scope parameter.
  3. The scopes_supported array 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.
  4. No scope parameter 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
{ "resource": "https://api.example.com/mcp", "authorization_servers": ["https://acme.okta.com/oauth2/aus1a2b3c"], "resource_name": "Acme MCP", "scopes_supported": ["mcp:access", "offline_access"] }

A request to /mcp without a bearer token receives:

Code
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp:access offline_access"

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.

Edit this page
Last modified on September 4, 2026
JWT Service PluginBuild with AI
On this page
  • Usage
  • Options
  • Advertising supported scopes
TypeScript
JSON