MCP Authorization Extensions and Enterprise Adoption
This guide explains authorization models beyond user delegation (machine-to-machine and enterprise-IdP-managed), and how to design authorization and governance when adopting MCP in an organization. Read MCP Authorization - OAuth 2.1 and Metadata Discovery first for the core authorization flow (authorization code + PKCE), and refer to MCP Security Best Practices - Attacks and Mitigations for attacks and mitigations.
1. What This Document Covers
- Role of authorization extensions
- OAuth Client Credentials
- Enterprise-Managed Authorization
- Choosing an authorization server
- Designing an MCP proxy server
- Per-tool scope design
- Gateways and MCP Server Portals
- Narrowing where a token can be used (sender-constrained tokens)
- Audit logs
Role of authorization extensions
The core authorization spec covers the common case: "a user interactively lets an MCP client access resources on their behalf." However, not every use of MCP fits this model.
- Machine-to-machine (M2M) integrations have no human involved. Background services, CI pipelines, and automated workflows must authenticate without interactive consent
- Enterprise environments have a centrally managed IdP that enforces policy across all apps. Making employees authorize each MCP server individually adds burden and bypasses existing security controls
The authorization extensions officially maintained in the ext-auth repository fill these two gaps. They have the following properties.
| Property | Description |
|---|---|
| Opt-in | Adoption is up to the implementation. They are not enabled by default |
| Additive | They add capabilities without changing or breaking core behavior |
| Composable | They are modular, so adopting several at once does not cause conflicts |
| Independently versioned | They follow the core's release cycle, but can be versioned on their own when needed |
Use of an extension is agreed through MCP extension negotiation. The client declares support in extensions under io.modelcontextprotocol/clientCapabilities in the _meta of each request, and the server advertises support in the capabilities returned by server/discover. If either side does not support an extension, it cannot be used. Support varies by client, so check the official Extension Support Matrix.
There are currently two extensions.
| Extension | Identifier | Problem it solves |
|---|---|---|
| OAuth Client Credentials | io.modelcontextprotocol/oauth-client-credentials | Machine-to-machine authentication with no user |
| Enterprise-Managed Authorization | io.modelcontextprotocol/enterprise-managed-authorization | Centralized access management by the enterprise IdP |
OAuth Client Credentials
This extension adds the OAuth 2.0 client credentials flow (RFC 6749 section 4.4) to MCP. The client authenticates with the application's own credentials rather than a user's delegated credentials, and the authorization server issues an access token without a browser redirect or any user interaction.
Typical use cases are as follows.
- Background services that call MCP tools on events or schedules
- CI/CD pipelines that call MCP servers as part of build, test, or deployment
- Server-to-server integrations between backends with no end user
- Daemons and long-running workers that need continuous access to MCP resources
For integrations where a human user should explicitly grant access, use the standard authorization flow.
There are two credential formats.
| Format | Description | Positioning |
|---|---|---|
| JWT Bearer Assertion (RFC 7523) | The client presents a JWT signed with its private key as proof of identity. The authorization server verifies the signature with the registered public key | Recommended. Short-lived, and the signing key itself is never sent |
| Client secret | client_id and client_secret are sent directly to the token endpoint | For simple setups |
A JWT assertion normally contains the following claims.
| Claim | Description |
|---|---|
iss | Client ID (issuer) |
sub | Client ID (the authenticated subject) |
aud | URL of the authorization server's token endpoint |
exp | Expiration time |
iat | Issued-at time |
A client secret is a long-lived credential that grants access without any user interaction. If it leaks, an attacker can authenticate as the application undetected until it is rotated. Store it in a secret manager (never in source code or version-controlled files), rotate it regularly and immediately if compromise is suspected, and narrow it to the minimum necessary scopes. Prefer JWT assertions where possible.
The implementation responsibilities are as follows.
| Implementer | Responsibility |
|---|---|
| Client | Declare the extension in the per-request capabilities. Obtain a token from the authorization server with client credentials before connecting to the MCP server. Send it with Authorization: Bearer. Token lifetimes are often shorter than for user-delegated tokens, so refresh before expiry |
| Server | On every request, validate the JWT's signature and claims with the authorization server's public key (usually from a JWKS endpoint). Check the scopes required for the operation. Advertising the extension in server/discover is recommended for discoverability |
The relationship to the core authorization spec is as follows. Core requirements such as token validation (including audience), sending the resource parameter, HTTPS, and keeping credentials out of logs are unchanged in this extension. Step-up is handled differently: a client_credentials client, which acts on its own credentials, MAY attempt re-authorization or MAY abort immediately.
Note that this token represents the client's authority, not a human's. The MCP server must design authorization and auditing on the assumption that sub identifies a service rather than a user (for example, do not let this token access per-user data).
Enterprise-Managed Authorization
This extension makes the enterprise IdP (Okta, Entra ID, an in-house SSO, and so on) the decision-maker for MCP authorization (SEP-990). In the standard model, users authorize each MCP client and each MCP server individually. That is ideal for consumer apps, but it causes the following problems in enterprises.
- Employees must understand the authorization details of every MCP server the organization uses
- When users authorize on their own, the security team cannot enforce consistent access policy
- At onboarding, dozens of services must be authorized manually
- At offboarding, access must be revoked individually in every service
With this extension, the IdP decides which employees may access an MCP server and under what conditions. Employees authenticate with the same corporate credentials they use for email and Slack, and the IdP allows or denies access based on organizational policy.
The mechanism is an application of the IETF draft "Identity Assertion JWT Authorization Grant (ID-JAG)." The MCP client obtains a special token called an ID-JAG from the IdP, presents it to the MCP server's authorization server, and exchanges it for an access token.
The flow has three stages.
- SSO: The user signs in to the MCP client with OpenID Connect or SAML, and the IdP issues an Identity Assertion (an ID token or a SAML assertion). The client stores it for later use. With SAML, the assertion can also be exchanged for a refresh token before use
- Token exchange (RFC 8693): The client presents the stored Identity Assertion to the IdP, specifies the identifier of the MCP server it wants to connect to, and obtains an ID-JAG. The IdP evaluates the policy defined by the administrator (groups, roles, conditional access, and so on)
- JWT Authorization Grant (RFC 7523): The client presents the ID-JAG to the token endpoint of the MCP server's authorization server and obtains an access token. The user is not redirected to the MCP authorization server's authorization endpoint
The main requirements the spec defines for this extension are as follows.
| Item | Requirement |
|---|---|
audience of the token exchange | The issuer identifier of the MCP server's authorization server (MUST) |
resource of the token exchange | Optional. If set, it MUST be the MCP server's resource identifier from RFC 9728 |
| ID-JAG | A JWT issued and signed by the IdP. The header typ is oauth-id-jag+jwt. If resource is present, it is the MCP server's resource identifier |
| Access token request | grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, with the ID-JAG as assertion |
| Access token | The audience MUST be restricted to the MCP server identified by the ID-JAG's resource |
| Discovering support | authorization_grant_profiles_supported in the authorization server metadata contains urn:ietf:params:oauth:grant-profile:id-jag |
The implementation responsibilities are as follows.
| Implementer | Responsibility |
|---|---|
| MCP client | Declare the extension. Support SSO with the enterprise IdP and store the Identity Assertion. When the server requires it, obtain an ID-JAG from the IdP and exchange it at the MCP authorization server. Let administrators configure the IdP endpoints as organization settings. Handle restrictions on the scopes of tokens issued by the IdP appropriately |
| MCP server | Indicate in the authorization metadata that it uses the extension. It may publish a resource descriptor so administrators can configure access policy in the IdP's admin console |
| MCP authorization server | Validate the ID-JAG issued by the IdP (signature via JWKS, audience, issuer, expiry). Map the ID-JAG claims (scope, resource) to permissions. For account linking, use sub as the primary stable identifier, and match against accounts created before the extension was configured by email |
Also be aware of the security assumptions and limits.
- Client registration: In many enterprises, IdP policy allows sign-in only for pre-registered clients. MCP clients must be pre-registered with the enterprise IdP for SSO
- Scope of visibility: The IdP sees only up to the issuance of the access token, not the actual traffic between the MCP client and server. What the IdP can enforce is policy on which users can use which servers through which clients (and, depending on the granularity of OAuth scopes, which scopes)
- Example policy: "Give the engineering group read-only access to the source-control MCP server, and give the marketing group read-write access to the internal documents"
- Revocation: An employee's access is revoked at the IdP, and this is reflected across all MCP clients. However, issued access tokens remain valid until they expire, so keep tokens short-lived
Choosing an authorization server
Protecting an MCP server requires an OAuth 2.1 authorization server. There are two broad options.
| Option | Suited for | Caveats |
|---|---|---|
| An existing IdP (Entra ID, Auth0, Keycloak, etc.) | The organization already has an IdP, and you want to centralize user management, MFA, and auditing | Check whether it supports the features MCP requires |
| Build your own authorization server with a library | The IdP does not support MCP requirements (CIMD, using resource as the audience, etc.), or you want freedom in designing the server-client relationship | You take on responsibility for issuing, storing, and revoking tokens |
Whichever you choose, verify the following.
| Item to check | Reason |
|---|---|
PKCE (S256) and code_challenge_methods_supported in the metadata | Clients do not proceed unless it is advertised |
Can it accept the RFC 8707 resource and reflect it in aud? | So the server can verify by audience that a token is meant for it. Some IdPs can only set a fixed audience |
| Support for Client ID Metadata Documents | The standard registration method for connecting with unrelated clients |
Including iss (RFC 9207) | So clients can prevent mix-up attacks |
| DCR controls | If used, decide on authentication, allowed hosts, and auditing of registrations. DCR is deprecated and its earliest removal is the first revision released on or after 2027-07-28, so also plan the move to CIMD |
| Access token lifetime and refresh token rotation | Limits the damage of a leak (rotating refresh tokens for public clients is a MUST) |
| Can one authorization server issue tokens for multiple MCP servers? | Consolidate while keeping audiences separated |
Workers OAuth Provider is a library for building an authorization server on Cloudflare Workers (@cloudflare/workers-oauth-provider). In v1, the authorization server (OAuthAuthorizationServer) and the MCP server (OAuthResourceServer) can be split into separate Workers. The MCP server has the authorization server validate tokens via a Service Binding, so validation does not traverse the public internet. It supports the MCP 2026-07-28 authorization spec (CIMD, iss), and insufficientScope() lets you write step-up in a single line. One authorization server can also issue tokens for multiple MCP servers. The consent screen and upstream sign-in helpers implement confused deputy mitigations.
The Cloudflare documentation describes four ways to use this library.
- Use Cloudflare Access as the OAuth provider
- Integrate directly with a third-party OAuth provider such as GitHub or Google
- Integrate with your own OAuth provider (including authorization services such as Stytch, Auth0, and WorkOS)
- Have the Worker itself handle all authorization and authentication
When using a third-party OAuth provider, per the MCP spec, the MCP server (Worker) issues its own token and hands it to the MCP client. It must not pass the upstream token through as is.
Designing an MCP proxy server
A setup that places an MCP server in front of a third-party API makes the client_id relationships complicated. There is a per-MCP-client client_id and a separate static client_id toward the third party, and this is a breeding ground for confused deputy attacks. The design principles are as follows.
- The proxy itself obtains consent, per MCP client, before forwarding to the third party
- Separate the token for the MCP client from the token used to call the upstream API, and never pass tokens through
- Store upstream tokens securely on the server side and do not hand them to the MCP client
- Match
redirect_uriexactly, and setstateafter consent and make it single-use
For concrete requirements and code examples, see the sections on confused deputy and token passthrough in MCP Security Best Practices - Attacks and Mitigations.
Per-tool scope design
When adopting MCP in an organization, express "who can use which tool" with scopes.
- Separate scopes for read, write, and destructive operations (for example,
tickets:read,tickets:write,tickets:delete) - Make the initial scopes cover only low-risk discovery and reading, and escalate step by step to privileged operations through a runtime 403
insufficient_scope - Do not create wildcard or all-permission scopes. Do not treat a scope as sufficient without server-side authorization logic
- Return the scopes required for the current operation together in a single challenge
The result of tools/list may vary with the authorization presented in the request (for example, returning only the tools permitted by the granted scopes). It must not vary per connection or as a side effect of other requests. Not returning tools the caller cannot see reduces the model's context and shrinks the attack surface. However, being visible and being permitted to execute are separate things, so validate the scopes and the user's permissions on every tools/call.
Gateways and MCP Server Portals
The state in which many MCP servers are used unmanaged within a company is called "Shadow MCP." The risk is employees connecting unmanaged local MCP servers to sensitive internal resources. MCP governance means administrators control which MCP servers the organization uses, who can use them, and under what conditions.
A gateway aggregates the organization's MCP servers into a single endpoint and centralizes authentication, policy, and auditing. Cloudflare's MCP Server Portals implement this role on top of Cloudflare Access.
| Feature | Description |
|---|---|
| Aggregation into one endpoint | Exposes multiple MCP servers (unauthenticated, OAuth-protected, and internal private servers) at a single portal URL (https://<portal-host>/mcp) |
| Portal sign-in | Users sign in with the IdP through Cloudflare Access. Each upstream server that requires OAuth asks for separate authentication |
| Policy | Defines who (users or groups) can use which tools under which conditions (device posture, location) |
| Tool filtering | Choose the tools and prompts to expose per portal. An allowlist approach is also possible (disabled by default, exposing only what is explicitly enabled) |
| Namespacing | Tools passing through a portal are prefixed with the server ID, as in {server_id}_{original tool name}, to prevent name collisions |
| Auditing | Access records MCP requests and tool executions that pass through the portal. Routing through Cloudflare Gateway also provides HTTP logs and DLP scanning |
| Machine-to-machine connections | Can connect with an Access service token instead of OAuth |
| Context optimization | Minimizing tool definitions and Code Mode can reduce token usage |
There are design caveats for portals.
- Even if you attach an Access policy to an individual MCP server, a blocked user can bypass the policy by connecting to the server's direct URL. Configure Access as that server's OAuth provider and enforce authentication on the server side as well
- MCP servers authorized through a portal do not get independent MFA, purpose entry, or temporary authentication enforced
- If you disable "Require user auth," everyone connected to the portal accesses the server with the server administrator's credentials. Per-user permissions and auditing are lost, so leave it enabled (the default)
- Renames of upstream servers' tools and prompts are reflected on sync. If you have set aliases, check them after every sync
- A portal is also a layer that relays access tokens. The portal's administrator privileges and the storage of upstream tokens become a new high-value target
Regardless of the type of gateway, aggregation gives you a single place for auditing and policy enforcement, but in exchange that place becomes a single point of failure and a target for compromise. Even with a gateway in place, do not skip token validation (audience, scopes) on each MCP server.
Narrowing where a token can be used (sender-constrained tokens)
A Bearer token can be used by anyone who holds it. Sender-constrained tokens (DPoP, mTLS), which bind a token to a specific client, limit the abuse of stolen tokens. However, the MCP 2026-07-28 authorization spec reviewed here has no provisions for DPoP or mTLS, and assumes access tokens are sent as Bearer. To use sender constraining with MCP, the client, authorization server, and MCP server must all support the same mechanism, and interoperability with standard clients cannot be expected.
The mitigations available within the spec are as follows.
- Make access tokens short-lived (the authorization server SHOULD). Rotate refresh tokens for public clients (MUST)
- Restrict tokens to a specific MCP server with
resource(audience) - Keep scopes minimal and escalate step by step
- Do not leave tokens in logs, query strings, or plaintext configuration files
For M2M integrations within an organization where you control the client, authorization server, and MCP server, there is room to consider sender constraining such as mTLS as an additional defense. For details of the mechanisms, see Sender-Constrained Tokens (DPoP / mTLS) and Refresh Token Rotation.
Audit logs
If you adopt MCP in an organization, you must be able to record who ran which tool, with which arguments, under which scope for incident investigation and compliance. The spec also asks clients to log tool usage for auditing (SHOULD), and the official MCP tutorial recommends that servers log scope-escalation events (the scopes requested and the subset granted) along with a correlation ID.
| Item | What to record |
|---|---|
| Subject | The user (sub), the client (client_id or azp), and whether it is M2M or delegated |
| Operation | Tool name, outcome (success, denied, error), and duration |
| Authorization | Token scopes, history of insufficient_scope and escalations, and policy decisions |
| Arguments | Values for non-sensitive fields; only hashes or field names for sensitive ones |
| Correlation | Request ID and correlation ID, for tracing across gateways, servers, and downstream APIs |
Do not record the tokens themselves, authorization codes, secrets, or the body of personal information. Collect logs in a tamper-resistant location and define retention periods and access rights. For detailed design, see MCP Observability and Auditing.
2. Authorization Methods by Scenario
| Scenario | Recommended method | Notes |
|---|---|---|
| Remote server acting with the user's permissions | Authorization code + PKCE (registered via CIMD) | The standard authorization flow. No extension needed |
| Machine-to-machine such as batch or CI | Client Credentials extension | Runs with the client's own permissions, not the user's. JWT assertions are recommended |
| Centralized access management by the enterprise IdP | Enterprise-Managed Authorization | Policy is controlled on the IdP side. Pre-register the client with the IdP |
| Aggregating many servers in the organization | Gateway (MCP Server Portals, etc.) | Centralizes authentication, policy, and auditing. Keep token validation on each server |
| Local stdio server | Take credentials from env vars or similar | Does not follow the OAuth spec |
| MCP server calling downstream APIs | Separate the MCP token from the downstream token | Token passthrough is forbidden |
3. Common Pitfalls for Developers
Using an extension without declaring it
Extensions are not enabled by default. The client declares them in the _meta capabilities, and the server also indicates support. Client support varies by implementation, so check the Extension Support Matrix before adopting.
Treating a Client Credentials token as the user's permissions
The subject of this token is the client (the service). If you let the service's permissions read per-user data in bulk, access control and auditing break down. Grant the service only the minimum scopes it needs, and if it must not access user data, enforce that on the server side.
Reusing or storing client secrets in configuration files
Long-lived credentials are abused silently when they leak. Issue a separate secret per service, keep it in a secret manager, and plan for rotation. With a JWT assertion, the signing key is never sent and the lifetime can be short.
Redirecting to the authorization endpoint in Enterprise-Managed Authorization
This extension does not redirect the user to the MCP authorization server's authorization endpoint. The ID-JAG is presented to the token endpoint as a jwt-bearer assertion.
Confusing the ID-JAG audience and resource
audience is the issuer of the MCP server's authorization server, and resource is the MCP server's resource identifier. The authorization server restricts the audience of the access token it issues to the MCP server identified by the ID-JAG's resource.
Trusting the ID-JAG iss too much
The authorization server decides the trusted IdP iss values with an allowlist and verifies signatures with that IdP's public key. With multi-tenant IdPs, pay attention not only to the issuer but also to the uniqueness of sub (sub is unique within an IdP and is identified in combination with iss).
Linking accounts by email alone
sub is the primary stable identifier. email is only a supplementary fallback for matching accounts created before the extension was configured.
Assuming IdP policy controls all MCP traffic
The IdP is involved only up to the issuance of the access token. It cannot control the content of subsequent MCP traffic or behavior within the token's lifetime. Combine short-lived tokens with scope validation on the MCP server.
Assuming a gateway makes server-side validation unnecessary
A server can still be called directly through a direct URL that bypasses the gateway, a gateway misconfiguration, or acting on behalf of an administrator's credentials. The server validates the audience, scopes, and user permissions itself.
Disabling "Require user auth" on a portal
You end up sharing the administrator's credentials, and the upstream server can no longer tell who performed which operation. Leave it enabled.
Putting arguments into audit logs as is
Arguments can contain personal information or secrets. Decide which fields to record with an allowlist, and record only hashes or field names for the rest.
Assuming DPoP makes MCP safe
The 2026-07-28 authorization spec has no provisions for DPoP, and standard clients and servers may not support it. First, be thorough about short-lived tokens, audience restriction, and minimal scopes.
4. Code Examples
Declaring an extension (client and server)
The client declares the extension in the _meta of each request (a Client Credentials example).
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/oauth-client-credentials": {}
}
}
}
}
}
The server advertises support in the capabilities of the server/discover response.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"extensions": {
"io.modelcontextprotocol/oauth-client-credentials": {}
}
}
}
}
For Enterprise-Managed Authorization, use the identifier io.modelcontextprotocol/enterprise-managed-authorization.
Client Credentials: a client with the official SDK (TypeScript)
The official TypeScript SDK obtains and refreshes tokens automatically. This example uses a JWT private key (use ClientCredentialsProvider for a secret).
import {
Client,
PrivateKeyJwtProvider,
StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";
const provider = new PrivateKeyJwtProvider({
clientId: "my-service",
privateKey: process.env.CLIENT_PRIVATE_KEY_PEM, // Inject from a secret manager
algorithm: "RS256",
});
const client = new Client(
{ name: "my-service", version: "1.0.0" },
{ capabilities: {} },
);
const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.example.com/mcp"),
{ authProvider: provider },
);
await client.connect(transport);
const tools = await client.listTools();
await transport.close();
Client Credentials: building a JWT assertion yourself (Go)
This is an example for when you cannot use an SDK. Send the signed JWT to the token endpoint as a jwt-bearer assertion, along with resource and scope. Keep the lifetime to a few minutes and change jti on every request.
package m2m
import (
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// NewAssertion creates a JWT assertion signed by the client itself.
func NewAssertion(clientID, tokenEndpoint string, key *rsa.PrivateKey) (string, error) {
jti := make([]byte, 16)
if _, err := rand.Read(jti); err != nil {
return "", err
}
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: clientID,
Subject: clientID,
Audience: jwt.ClaimStrings{tokenEndpoint}, // The authorization server's token endpoint
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)), // Keep it short-lived
ID: hex.EncodeToString(jti),
}
return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key)
}
// TokenRequest builds the request to the token endpoint.
// resource is the MCP server's canonical URI. Always send it, whether or not the authorization server supports it.
func TokenRequest(tokenEndpoint, assertion, resource, scope string) (*http.Request, error) {
form := url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
"assertion": {assertion},
"resource": {resource},
"scope": {scope},
}
req, err := http.NewRequest(http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
Enterprise-Managed Authorization: ID-JAG token exchange
This is the token exchange request the client sends to the IdP's token endpoint (an example from the official spec). audience is the MCP authorization server's issuer, and resource is the MCP server's resource identifier.
POST /oauth2/token HTTP/1.1
Host: acme.idp.example
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&audience=https://auth.chat.example/
&resource=https://mcp.chat.example/
&scope=chat.read+chat.history
&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_id=2ec954a1d60620116d36d9ceb7
&client_secret=...
The ID-JAG the IdP issues is a JWT with claims like the following (the header typ is oauth-id-jag+jwt).
{
"jti": "9e43f81b64a33f20116179",
"iss": "https://acme.idp.example",
"sub": "U019488227",
"aud": "https://auth.chat.example/",
"resource": "https://mcp.chat.example/",
"client_id": "f53f191f9311af35",
"exp": 1311281970,
"iat": 1311280970,
"scope": "chat.read chat.history"
}
The client presents this ID-JAG to the MCP authorization server as follows.
POST /oauth2/token HTTP/1.1
Host: auth.chat.example
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...
&client_id=https://client.example.com/client.json
Enterprise-Managed Authorization: validating an ID-JAG at the authorization server (Go)
Accept only iss values on the trusted-IdP allowlist, verify the signature with that IdP's key, and check typ, aud, expiry, and resource. Restrict the audience of the issued access token to the MCP server identified by resource.
package idjag
import (
"errors"
"fmt"
"slices"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
jwt.RegisteredClaims
Email string `json:"email,omitempty"`
Resource string `json:"resource,omitempty"`
ClientID string `json:"client_id,omitempty"`
Scope string `json:"scope,omitempty"`
}
type Validator struct {
Issuer string // This authorization server's issuer. Must match the ID-JAG's aud
TrustedIdP map[string]jwt.Keyfunc // Maps each trusted IdP's iss to a function returning its key (JWKS)
Resources []string // Resource identifiers of the MCP servers this authorization server issues tokens for
}
func (v *Validator) Validate(raw string) (*Claims, error) {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"RS256", "ES256"}),
jwt.WithAudience(v.Issuer),
jwt.WithExpirationRequired(),
)
claims := &Claims{}
_, err := parser.ParseWithClaims(raw, claims, func(t *jwt.Token) (any, error) {
if typ, _ := t.Header["typ"].(string); typ != "oauth-id-jag+jwt" {
return nil, fmt.Errorf("unexpected typ %q", typ)
}
c, ok := t.Claims.(*Claims)
if !ok {
return nil, errors.New("unexpected claims")
}
// Use only keys of IdPs on the allowlist to verify the signature.
keyfunc, ok := v.TrustedIdP[c.Issuer]
if !ok {
return nil, fmt.Errorf("untrusted issuer %q", c.Issuer)
}
return keyfunc(t)
})
if err != nil {
return nil, err
}
if claims.Subject == "" {
return nil, errors.New("missing sub")
}
// Restrict the access token's audience to the MCP server identified by resource.
if claims.Resource == "" || !slices.Contains(v.Resources, claims.Resource) {
return nil, fmt.Errorf("unknown resource %q", claims.Resource)
}
return claims, nil
}
After successful validation, identify the user by the pair of iss and sub, and use email only as a supplement when linking to existing accounts. Cap the granted scopes at the ID-JAG's scope, and narrow them further with this authorization server's own policy.
Enforcing required scopes per tool on the server (Go)
Keep a mapping from tool names to required scopes and validate it on every tools/call. If scopes are insufficient, return 403 insufficient_scope with all the scopes needed for the current operation. Unregistered tools are rejected.
package toolauthz
import (
"fmt"
"net/http"
"slices"
"strings"
)
// Declared by the operator after review. Unregistered tools cannot be called (deny by default).
var requiredScopes = map[string][]string{
"tickets_search": {"tickets:read"},
"tickets_update": {"tickets:read", "tickets:write"},
"tickets_delete": {"tickets:read", "tickets:write", "tickets:delete"},
}
// Check verifies whether the token's scopes (space-separated) are sufficient for the tool.
// If not, it returns the WWW-Authenticate value in challenge.
func Check(tool, tokenScope, metadataURL string) (status int, challenge string) {
need, ok := requiredScopes[tool]
if !ok {
return http.StatusForbidden, `Bearer error="insufficient_scope"`
}
have := strings.Fields(tokenScope)
for _, s := range need {
if !slices.Contains(have, s) {
return http.StatusForbidden, fmt.Sprintf(
`Bearer error="insufficient_scope", scope=%q, resource_metadata=%q`,
strings.Join(need, " "), metadataURL)
}
}
return http.StatusOK, ""
}
Workers OAuth Provider: separating the authorization server and the MCP server
This is an example of the v1 split API from the official changelog (key points only). The authorization server Worker issues tokens, and the MCP server Worker validates them via a Service Binding. For writes, it requests step-up with insufficientScope().
import {
OAuthAuthorizationServer,
OAuthResourceServer,
insufficientScope,
} from "@cloudflare/workers-oauth-provider";
import { WorkerEntrypoint } from "cloudflare:workers";
// Authorization server Worker: user sign-in and token issuance
const authorizationServer = new OAuthAuthorizationServer<Env>({
issuer: "https://auth.example.com",
resources: ["https://calendar.example.com/mcp"],
scopesSupported: ["calendar:read", "calendar:write", "offline_access"],
clientIdMetadataDocumentEnabled: true,
});
export class AuthServer extends WorkerEntrypoint<Env> {
fetch(request: Request) {
if (new URL(request.url).pathname === "/authorize") {
return showConsent(request, this.env); // Your own consent screen
}
return authorizationServer.fetch(request, this.env, this.ctx);
}
validateToken(resource: string, token: string) {
return authorizationServer.validateToken(resource, token, this.env);
}
}
// MCP server Worker: has the authorization server validate tokens via a Service Binding
export const calendar = new OAuthResourceServer<Env, AuthProps>({
resourceMetadata: {
resource: "https://calendar.example.com/mcp",
authorization_servers: ["https://auth.example.com"],
},
requiredScopes: ["calendar:read"],
validateToken: (env) => env.AUTH_SERVER.validateToken,
handler: {
fetch(request, env, ctx) {
if (request.method === "POST" && !ctx.auth.scope.includes("calendar:write")) {
return insufficientScope(ctx.auth, ["calendar:read", "calendar:write"]);
}
return handleMcp(request, ctx.props);
},
},
});
MCP Server Portals: restricting exposed tools with an allowlist
With the portal API, disable the server by default (default_disabled) and enable only the tools to expose with updated_tools. When you pass servers in a portal update, the array replaces the entire server-to-portal mapping, so include all connected servers and the existing settings.
{
"servers": [
{
"server_id": "example-server",
"default_disabled": true,
"updated_tools": [
{ "name": "search_documents", "enabled": true },
{ "name": "list_projects", "enabled": true }
]
}
]
}
Emitting structured audit logs (Go)
Record argument values only for allowlisted fields, and only field names and hashes for the others. Tokens are not recorded.
package audit
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log/slog"
"sort"
"time"
)
// Non-sensitive argument fields whose values may be recorded as is.
var loggableArgs = map[string]bool{"status": true, "limit": true, "project": true}
type Event struct {
RequestID string
Subject string // The token's sub. For M2M, the client itself
ClientID string
Delegated bool // true for user delegation, false for client credentials
Tool string
Scope string
Args map[string]any
Outcome string // ok / denied / error
Duration time.Duration
}
func Log(l *slog.Logger, e Event) {
args := make([]any, 0, len(e.Args))
keys := make([]string, 0, len(e.Args))
for k := range e.Args {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if loggableArgs[k] {
args = append(args, slog.Any(k, e.Args[k]))
continue
}
sum := sha256.Sum256([]byte(k + "=" + toString(e.Args[k])))
args = append(args, slog.String(k+"_sha256", hex.EncodeToString(sum[:8])))
}
l.Info("mcp.tool_call",
slog.String("request_id", e.RequestID),
slog.String("sub", e.Subject),
slog.String("client_id", e.ClientID),
slog.Bool("delegated", e.Delegated),
slog.String("tool", e.Tool),
slog.String("scope", e.Scope),
slog.Group("args", args...),
slog.String("outcome", e.Outcome),
slog.Duration("duration", e.Duration),
)
}
func toString(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
5. Summary
- Authorization extensions are opt-in, additive, composable, and independently versioned additions to the core authorization spec. The client declares them in
_metaand the server indicates support inserver/discover. They are not enabled by default, and client support varies - Client Credentials is for M2M with no user, and the token's subject is the client itself. JWT Bearer Assertion (RFC 7523) is recommended; client secrets are long-lived credentials to be managed in a secret manager and rotated
- With Enterprise-Managed Authorization, the enterprise IdP controls access through the ID-JAG. The client obtains an ID-JAG through token exchange at the IdP and presents it to the MCP authorization server's token endpoint. The IdP is involved only up to token issuance and cannot see the traffic content
- The authorization server is either an existing IdP or one built with a library. Check PKCE, using
resourceas the audience, CIMD,iss, DCR controls, and token lifetimes. Workers OAuth Provider v1 can separate the authorization server and the MCP server - A proxy obtains per-client consent and separates the MCP token from the downstream token. Per-tool scopes are split into read, write, and destructive operations, and validated on every
tools/call - Gateways and MCP Server Portals centralize authentication, policy, and auditing, and prevent Shadow MCP. However, keep token validation on each server and leave the portal's "Require user auth" enabled
- DPoP and mTLS are not specified in the MCP authorization spec reviewed here. Prioritize short-lived tokens, audience restriction, and minimal scopes
- Audit logs record who ran which tool, with which arguments, under which scope, along with a correlation ID. Do not record tokens or sensitive arguments
Related Documents
- MCP Authorization - OAuth 2.1 and Metadata Discovery
- MCP Security Best Practices - Attacks and Mitigations
- MCP Observability and Auditing
- Sender-Constrained Tokens (DPoP / mTLS) and Refresh Token Rotation
- Authorization with OPA (Open Policy Agent)
References
- Authorization Extensions
- OAuth Client Credentials
- Enterprise-Managed Authorization
- Enterprise-Managed Authorization (specification)
- SEP-990: Enterprise IdP policy controls
- Identity Assertion JWT Authorization Grant (IETF draft)
- RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants
- RFC 8693: OAuth 2.0 Token Exchange
- Workers OAuth Provider
- Workers OAuth Provider v1 (Cloudflare Changelog)
- Authorization (Cloudflare Agents)
- MCP governance (Cloudflare Agents)
- MCP server portals (Cloudflare Access)