Proposal: Fine-Grained Resource Control for Multi-User Authorization #483
Replies: 4 comments 4 replies
-
|
Thanks for the proposal @adranwit — it's clearly well thought out. However, it seems like this proposal still expects the MCP client to have the tokens that the MCP server needs to authorize against other downstream services as part of tools. Is my understanding correct there? If so, that is a dangerous security vulnerability as it mixes roles and exposes the tokens that the MCP server should be the OAuth client for and the MCP client should never have access to. This is a critical requirement in the OAuth security boundary. As an alternative, I think we can use the existing MCP Authorization spec to provide fine grained authorization from MCP client to MCP server for each resource required. Downstream-tool auth can be handled via OAuth token exchange (where supported). And, in the future, the identity assertion grant, which will improve the user experience. When token exchange is not supported, we are proposing a way for the MCP server to request user interaction in #475 |
Beta Was this translation helpful? Give feedback.
-
Agreed, but just to be clear: with respect to "downstream" resources (like a third-party resource server), the MCP server is the OAuth client. The MCP server is also an OAuth client (with respect to the MCP server only), but it is a public client by definition because it often is a browser app or desktop app.
I think this is the right way to do it. 👍 By preserving the security boundary around the MCP server, it also means that there is no need to publish metadata like required scopes per-tool/resource. If you generalize it a tiny bit further to just "the MCP server sends a URL to the MCP client", it's a lot like what @wdawson and I wrote up in #475. I'd love your comments! |
Beta Was this translation helpful? Give feedback.
-
|
Updated various Client-Side strategy implementation with Backend For Frontend flow. |
Beta Was this translation helpful? Give feedback.
-
|
For what it's worth, architecture diagram with the MCP server -> Authorization server bears a lot of resemblance to Kubernetes admission control. Kubernetes supports policies directly, but also supports a webhook as a pressure-relief valve for more complex or newly emerging patterns. Notably, K8s added policy support later because the webhooks became a reliability issue, and common usage patterns were better understood by that point. For inspiration, consider: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers Additionally, signed access conditions attached to an opaque token is sort of how things like Google's Credential Access Boundaries work: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials In all likelihood, you'll want both concepts. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Pre-submission Checklist
Your Idea
Fine-Grained Resource Control for Multi-User Authorization
Motivation
Many MCP clients require per-user credentials to call downstream APIs, and the lack of a standard leads to inconsistent
client behavior and security gaps. This proposal standardizes OAuth-based authorization in MCP.
Background
Multiple ideas have been discussed in the MCP community about how to handle authorization for tools and resources in
multi-user scenarios:
Per-Tenant Configuration via _meta: One proposal
Discussion #193 suggested allowing a
single MCP server instance to serve many end-users by passing a clientId and user-specific clientConfig in each
request
instead of requiring separate server instances.
dynamically inject config for each end-user.
MCP Server as OAuth 2.0 Resource Server:
Issue #205 proposed that MCP servers
should act as OAuth 2.0 resource servers, using external identity providers for authorization.
token from an OAuth authorization server (via any standard flow) and presents it with requests
WWW-Authenticate challenge or providing discovery info – to prompt the client to get proper tokens
stateless
behalf of the user
behalf of the user, obtaining a delegated token for downstream APIs.
On-Behalf-Of (OBO) Token Exchange: Related to the above, discussion
Support On-Behalf-Of Token Exchange protocol for Agent-to-Agent Communications #214(Support On-Behalf-Of Token Exchange protocol for Agent-to-Agent Communications #214) raised support for OAuth 2.0 Token
Exchange (RFC 8693) to avoid passing raw user tokens around. The idea is to enable delegation: an agent client might
present one token to an MCP server, which exchanges it for a new token restricted to the server’s context (preventing
misuse of the original token). This was motivated by security concerns that simply forwarding user tokens is risky
instead of exposing the user’s Google API token, thereby limiting an attacker’s capabilities if the client token
is compromised
server’s constrained tools, not the upstream service broadly
Per-Tool OAuth Scopes (Multi-User Auth): In discussion
Multi-user Authorization #234(Multi-user Authorization #234), wdawson proposed adding an
authorization spec to each tool definition to declare what kind of token and scopes it requires
at call time in the request metadata
server persisting those credentials. The proposal introduced a JSON-RPC error code for authorization failures
(
-32001for missing or invalid tokens), analogous to HTTP 401 Unauthorized.management to the client
Client vs. Server Responsibility Debate: There is ongoing discussion about the trade-offs of the client-managed
token approach.
the same time, concerns were raised about security and complexity for client developers. For instance, if each
tool integration requires the client to implement a different OAuth flow, it burdens agent developers and could
discourage use of certain tools
for example, by providing an authorization URL or instructions if a token is missing. Others pointed out that
standard OAuth consent flows already allow users to grant a subset of scopes, and the server could simply enforce
scope requirements (skipping or failing a tool call if not authorized) without additional protocol changes
the client for storage , combining a smoother user onboarding with client-side token storage thereafter.
In summary, the community has explored per-tool and per-resource auth scopes, multi-tenant call metadata, client vs
server auth roles, token exchange, and error handling for auth. Building on those ideas, this proposal aims to
consolidate a path forward for fine-grained resource control in MCP, aligning with established terminology and
extending the protocol where needed.
Proposal Summary
Per-Tool / Resource Auth Metadata
Each tool or resource declares its own authorization requirements via a separate policy. The MCP server is responsible for enforcing this policy.
protectedResourceMetadataauthorization_servers.required_scopesuse_id_tokentrue, the system requests an ID token instead of or in addition to an access token.client_idBackend-for-Frontend (BFF) Architecture with Resource-Bound Access Tokens
To enforce strong security boundaries and maintain control over OAuth flows, the MCP architecture adopts a Backend-for-Frontend (BFF) pattern.
In this model, the MCP Server acts as an intermediary between the MCP Client (typically a browser or desktop application) and downstream protected resources.
Key Characteristics
MCP Client is Lightweight
The client (frontend) does not directly handle sensitive tokens or secrets. It initiates OAuth flows via URLs provided by the MCP Server, but does not retain or manage access tokens.
MCP Server as the Confidential Client
The MCP Server is the registered OAuth 2.0 confidential client. It performs the token exchange, holds client credentials, and manages scopes.
Resource-Bound Access Tokens
Each issued access token is explicitly bound to a single resource (audience). This means:
Initiation Flow
When the MCP Client requires access:
/authorizeURL with the appropriateclient_id,scope,resource, andcode_challenge.No Token or Secret Exposure to Client
The frontend never sees client secrets, access tokens, or refresh tokens. This maintains a strong separation of concerns and reduces the attack surface.
--
Two Authorization Mediation Modes
The MCP server supports two mediation flows for initiating OAuth authorization when credentials are missing or insufficient:
1. HTTP SSE Enforced Security
HTTP/1.1 401 Unauthorized
X-Authorization-Exchange: auth_code:, redirect_uri: ...
2. JSON-RPC Enforced Security
tools/call,resource/readmethod is invoked without valid credentials, the server responds with a structured authorization error:{ "jsonrpc": "2.0", "id": 42, "error": { "code": -32001, "message": "Unauthorized", "data": { "authorizationUri": ".../authorizer?resource=…,scope=…,client_id=…,code_challenge=…,code_challenge_method=…,redirect_uri=…,state=…" } } }{ "jsonrpc": "2.0", "id": 42, "method": "tools/call", "params": { "name": "myTool", "arguments": {}, "_meta": { "authorization": { "authCode": "xxxx...", "redirectUri": "..." } } } }💡 These two modes are proposed because MCP may operate over multiple transports—including non-HTTP channels like stdio—where traditional HTTP status codes and headers (e.g., 401 Unauthorized, WWW-Authenticate) are not applicable.
In those environments, JSON-RPC error responses serve the same purpose: to deliver authorization metadata and trigger the OAuth flow.
This dual-mode approach ensures consistent security regardless of the transport protocol.
Token Redemption & Caching (MCP Server Side)
client_id) at the AS.Proposal Details
Centralized Authorization Policy bounded to Global MCP, Tools and Resources
Idea: Every tool or resource advertises the auth rule it needs.
/.well-known/oauth-protected-resource/resource=RESOURCE_URI).true→ supply an OpenID Connect ID token instead of / in addition to an OAuth access-token.Authorization rules are defined in a separate configuration schema that maps to tools, resources, or global defaults.
{ "global": { "protectedResourceMetadata": { "resource": "MCPServer", "authorizationServers": [ "https://auth.acme-cloud.com" ] }, "requiredScopes": [ "scope1" ], "useIdToken": false, "clientId": "myClientId1" }, "tools": [ { "protectedResourceMetadata": { "resource": "myTool", "authorizationServers": [] }, "requiredScopes": [ "scope1" ], "clientId": "myClientId2" } ], "resources": [ { "protectedResourceMetadata": { "resource": "s3://myBucketX/asset", "authorizationServers": [] }, "requiredScopes": [ "read_write" ], "useIdToken": true }, { "protectedResourceMetadata": { "resource": "gs://myBucketY/asset", "authorizationServers": [] }, "requiredScopes": [ "read" ], "useIdToken": true, "clientId": "myClientId3" } ] }Each entry in this external policy schema corresponds to the Protected Resource Metadata (RFC 9728) and is extended with MCP-specific fields:
requiredScopes: the minimal OAuth scopes that the client must present.useIdToken: a boolean indicating if an OpenID Connect ID token should be used instead of (or in addition to) anaccess token.
clientId: the client ID to use for the OAuth2 authorization server.This unified representation lets operators configure resource metadata and per-resource authorization requirements in
one place, without modifying individual tool definitions.
Backend-for-Frontend (BFF) Architecture with Resource-Bound Access Tokens
To authorize access to MCP tools and resources, this proposal adopts a Backend-for-Frontend (BFF) architecture with resource-bound access tokens. This ensures that tokens are only valid for their intended audience and cannot be reused across services.
Clients do not proactively fetch authorization policy. Instead, when calling a tool without prior authorization, the MCP server responds with a
401 Unauthorizederror that includes the necessary authorization metadata. The client then uses this metadata to initiate the authorization flow.Specifically, the MCP client launches an OAuth 2.0 Authorization Code flow with PKCE, using the provided parameters such as
client_id,resource,code_challenge, andstate.After user authorization, the client receives an authorization code. The MCP server then redeems the code—along with the code verifier, client credentials, and any required parameters
a) HTTP SSE Enforced Security — Sequence Diagram
sequenceDiagram participant MCP_Client as MCP Client participant OAuth2_Client as OAuth2 Client participant MCP_Server as MCP Server participant Auth_Server as Authorization Server participant Resource as Protected Resource MCP_Client->>MCP_Server: tools/list MCP_Server-->>MCP_Client: tool list MCP_Client->>MCP_Server: tools/call (not authorized yet) MCP_Server-->>MCP_Client: error 401 WWW-Authorize authorization_uri="..." MCP_Client->>Auth_Server: Authorization Request (resource, client_id, PKCE, state, scope, redirect_uri) Auth_Server-->MCP_Client: auth code MCP_Client->>MCP_Server: tools/call (with token in X-Authorization-Exchange: auth_code=code, redirect_uri=https://localhost:port/callback)} OAuth2_Client->>Auth_Server: Token Request (auth_code + PKCE verifier) Auth_Server-->>OAuth2_Client: Access Token OAuth2_Client-->>MCP_Server: return access token MCP_Server->>Resource: tool invocation with bearer token Resource-->>MCP_Server: execution result MCP_Server-->>MCP_Client: resultb) JSON-RPC Enforced Security
sequenceDiagram participant MCP_Client as MCP Client participant OAuth2_Client as OAuth2 Client participant MCP_Server as MCP Server participant Auth_Server as Authorization Server participant Resource as Protected Resource MCP_Client->>MCP_Server: tools/list MCP_Server-->>MCP_Client: tool list MCP_Client->>MCP_Server: tools/call (not authorized yet) MCP_Server-->>MCP_Client: error.code -32001 error.data=(authorizationUri="...") MCP_Client->>Auth_Server: Authorization Request (resource, client_id, PKCE, state, scope, redirect_uri) Auth_Server-->MCP_Client: auth code MCP_Client->>MCP_Server: tools/call (with token in _meta.authorization(authCode=code, redirectUri=https://localhost:port/callback) OAuth2_Client->>Auth_Server: Token Request (code + PKCE verifier) Auth_Server-->>OAuth2_Client: Access Token OAuth2_Client-->>MCP_Server: return access token MCP_Server->>Resource: tool invocation with bearer token Resource-->>MCP_Server: execution result MCP_Server-->>MCP_Client: resultClient-Side Implementation
When a tool requires authorization, the MCP client starts the OAuth 2.0 Authorization-Code flow with PKCE.
To capture the authorization response, you MAY choose one of three strategies:
code?http://127.0.0.1:{port}/callbackpostMessage)https://sdk.example.com/oauth/relay.htmlhttps://mcp.example.com/oauth/callbackOption A – Loopback Listener (RFC 8252)
http://127.0.0.1:38545/callback./authorizewithclient_id,state, PKCE params, and the loopbackredirect_uri.GET /callback?code=…&state=…
/token, passing the sameredirect_uri.Option B – Web-Message (
postMessage) RedirectOpen a browser (or popup) to
/authorizewithresponse_mode=web_messageor redirect_uri=https://sdk.example.com/oauth/relay.htmlAuthorization server redirects to a minimal
relay.htmlthat immediately executes:/token.Option C – Direct MCP Redirect (client bypass)
/tokenNo X-Authorization-Exchange header is needed in this option because the client never sees the code.
Note that this option can be implemented with user interaction as proposed in #475
Passing
/authorizedata over SSE: custom header?The BFF pattern doesn’t specify a standard way to forward the OAuth authorization code, so one idea is to place it in a custom header such as
X-Authorization-Exchange.auth_codeandredirect_uritravel in a dedicated header, keeping application-level SSE payloads clean.Authorization: Bearer– Because the code is only an intermediate artifact, labeling it as a full access token would be misleading.Alternative Approaches Considered (As Per JSON-RPC)
1. Inline Metadata in Request Body or Payload
{ "request": { ... }, "_meta": { "authorization": { "authCode": "xyz", "redirectUri": "http://127.0.0.1:38545/callback" } } }Security Considerations
References
Design Considerations and Trade-offs
Initial proposals favored having clients handle the entire OAuth 2.0 flow and manage token storage. However, as @wdawson
pointed out, this approach introduces security vulnerabilities (the "confused deputy" problem).
The updated design delegates token management to the MCP server, except for initiating the OAuth 2.0 authorization code
flow: the server provides the client with the authorization endpoint, client ID, and PKCE parameters, and the client
returns the authorization code. The MCP server then redeems the code to obtain an access token, caches it, and refreshes
it as needed.
This proposal takes a pragmatic approach by treating the MCP client as an OAuth 2.0 client and the MCP server as an
OAuth 2.0 resource server, leveraging standard flows and existing infrastructure.
In practice, agents acting on behalf of users are already trusted with user data and actions, so they are responsible
for securely initiating the authorization flow.
Scope
Beta Was this translation helpful? Give feedback.
All reactions