Development Choices

Bind OAuth Tokens to the Intended MCP Server

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
12 min read3 sources cited

Include the MCP server’s canonical URI as the OAuth `resource` in both authorization and token requests. Configure the server to accept only tokens issued for that audience, then obtain separate upstream credentials rather than forwarding the client token. Test wrong-audience tokens and passthrough paths before release.

Prerequisites

An MCP server validates its own client token and obtains a separate token for an upstream API
Each resource receives a token issued specifically for its audience.

This procedure applies to a protected MCP server using an HTTP-based transport. Authorization is optional in MCP, and the OAuth flow described here is not intended for a stdio transport, where credentials should instead come from the environment.

Before changing the flow, identify the four roles separately:

You need control over the client’s authorization and token requests, the MCP server’s token validation, and—if the server calls an upstream API—its upstream credential flow. If you are still deciding whether a remote server should use OAuth or fixed credentials, settle that boundary first with the conditions in OAuth versus API key headers for an MCP server.

The requirements below follow the MCP Authorization specification dated 2025-11-25. In that model, the MCP client is an OAuth client, the protected MCP server is the OAuth resource server, and the authorization server issues access tokens for use at that MCP server.

Steps

  1. Draw the token boundaries before changing requests

    Treat the client-to-MCP-server token and any MCP-server-to-upstream token as different credentials for different audiences.

    The first credential authorizes the MCP client to call one MCP server. Its intended audience is that MCP server. If the MCP server later calls a third-party API, the third-party API is another resource server and requires a credential issued for that resource. Sharing an OAuth provider, user, scope name, or bearer-token format does not merge those two trust boundaries.

    Write down the path explicitly:

    MCP client
      └─ token A, audience: MCP server
           → MCP server
                └─ token B, audience: upstream API
                     → upstream API

    This distinction decides where validation and token acquisition belong. The MCP server validates token A before processing the MCP request. It then uses its own upstream authorization arrangement to obtain or retrieve token B. It does not turn token A into token B by forwarding it.

    A server that has no upstream API needs only the first boundary. A proxy-style server needs both. That extra boundary costs another credential flow and associated server-side state, but omitting it makes it impossible to show that each bearer token was issued for the service receiving it.

  2. Choose one canonical resource URI for the MCP server

    Set the OAuth resource value to the canonical URI identifying the MCP server. Under RFC 8707 Resource Indicators for OAuth 2.0, the value is an absolute URI identifying the target service. It must not contain a fragment and should identify the complete API or resource set as specifically as the client can.

    Use the path when it distinguishes this MCP server from other applications or MCP servers on the same host. For example:

    https://mcp.example.com/mcp
    https://mcp.example.com:8443
    https://mcp.example.com/media/mcp

    A bare host name such as mcp.example.com is not valid because it lacks a URI scheme. A value such as https://mcp.example.com#tools is not valid because it contains a fragment.

    Avoid choosing the host root merely because it is shorter. If /media/mcp and /files/mcp identify separate protected resources, using the path gives the authorization server enough information to distinguish them. RFC 8707 also calls for specificity when a path identifies a tenant in a multi-tenant service: an audience restriction cannot separate tenants if the resource identifier erases the tenant boundary.

    Decide whether a trailing slash is semantically significant, then use the same form everywhere. The MCP specification recommends the form without a trailing slash when the slash has no meaning. A difference between https://mcp.example.com and https://mcp.example.com/ can otherwise become an avoidable interoperability failure.

    Record the chosen URI in the client configuration, authorization-server configuration, protected-resource metadata, and server validation policy. Do not maintain separate handwritten variants for those locations. If you are configuring a concrete remote endpoint such as Cloudinary’s workflow surface, apply the same canonical-URI decision while following the service-specific connection details in configuring the MediaFlows MCP server.

  3. Send resource in the authorization request

    Add the canonical MCP server URI to the request sent to the authorization endpoint. For a server at https://mcp.example.com/mcp, the relevant part of an authorization-code request is:

    GET /authorize?
      response_type=code&
      client_id=example-client&
      redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
      resource=https%3A%2F%2Fmcp.example.com%2Fmcp

    The MCP client must include this parameter even if it does not know whether the authorization server currently uses Resource Indicators. This is a client requirement, not a capability to enable only after discovering server support.

    The parameter answers where the token will be redeemed. It is not a substitute for scope, which answers what access is requested. A scope such as files:read may describe an operation without identifying whether the token is for the files MCP server, another service on the same host, or an upstream API. Keep both concepts in the request when both are needed.

    Preserve the exact resource value through redirects and stored authorization state. Do not derive a broader value from the authorization-server host, client redirect URI, or scope. Those values identify other parts of the transaction.

    If the authorization server rejects a missing, unknown, malformed, or unacceptable target, RFC 8707 defines invalid_target for that condition. Treat that as a configuration failure: compare the requested URI with the resource registered or recognized by the authorization server. Do not respond by silently dropping resource, because the MCP client is still required to send it.

  4. Repeat the same resource in the token request

    Include the canonical MCP server URI again when exchanging the authorization code at the token endpoint:

    POST /token HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code&
    code=returned-code&
    redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
    resource=https%3A%2F%2Fmcp.example.com%2Fmcp

    MCP requires resource in both the authorization request and the token request. Sending it only during consent is incomplete: the token request is where the client identifies the service at which the newly requested access token will be used.

    Apply the same rule when obtaining a new access token with a refresh token. The refresh grant may represent a broader authorization, while the access token requested from it is for the resource named in that token request. Keep the canonical MCP server URI attached to the client’s refresh path rather than relying on an authorization-server default.

    Request one MCP resource unless the deployment deliberately needs a multi-audience token and all recipients share the necessary trust. RFC 8707 encourages a single resource: when a bearer token has several audiences, any recipient that receives it may be able to present it to another listed recipient. A single audience keeps the usable destination narrow and makes negative testing straightforward.

    This step costs a field in each grant-handling path, including refresh and reauthorization. Missing one path creates intermittent behavior: the initial exchange may produce a bound token while a later refresh produces a token under a different default. Test every path that can mint a replacement access token.

  5. Configure the authorization server to issue for that audience

    Configure the authorization server to recognize the canonical MCP resource and audience-restrict the resulting access token to it. RFC 8707 allows the server to use the exact resource value as the audience or map it to another URI or abstract identifier. Whichever representation you choose becomes the value the MCP server must validate.

    With a JWT access token, the audience restriction can be represented by the aud claim. With an opaque token, the authorization server can expose audience information through token introspection. The token format is not the security property; the enforceable binding between the issued token and the intended MCP server is.

    Define that mapping once. For example, if clients send resource=https://mcp.example.com/media/mcp but the authorization server emits aud=mcp-media, configure the MCP server to require mcp-media. Do not make the server guess between the canonical URI, host name, client ID, and scope.

    Reject resource values the authorization server does not recognize rather than issuing a general bearer token. A token issued without a specific audience may still look syntactically valid, but it does not establish that the MCP server is its intended recipient.

    When one authorization server serves several MCP servers, register each resource separately. That is the condition in which resource indicators matter most: authorization-server identity alone cannot tell one receiving resource whether the token was issued for it or for its neighbor.

  6. Make the MCP server enforce its own audience

    Validate every access token before executing a tool, reading a resource, or producing a prompt result. The MCP server must reject tokens that were not issued for its own audience. It must not accept a token merely because the token is well formed, comes from a familiar authorization server, contains a useful scope, or names a recognized user.

    Audience validation is a recipient check. A valid token for https://mcp.example.com/files/mcp remains invalid at https://mcp.example.com/media/mcp. A scope shared by both services does not change that result.

    Apply validation to every HTTP request, even when several requests belong to the same logical MCP session. The bearer token belongs in the Authorization header:

    Authorization: Bearer <access-token>

    Do not place it in the URI query string. Do not use an MCP session identifier as a replacement for authenticating later requests.

    Return HTTP 401 when a token is invalid, expired, or has the wrong audience. Reserve HTTP 403 for a valid token that lacks the scope needed for the attempted operation. Use HTTP 400 for a malformed authorization request. Keeping these cases separate matters operationally: acquiring more scope cannot repair a wrong audience, and repeating the same token cannot repair expiration.

    When a production call fails, compare the requested resource, the token’s effective audience, and the MCP server’s expected audience before changing scopes. The sequence in diagnosing a failing MCP tool call helps keep transport, authorization, and tool failures separate.

  7. Replace token passthrough with a separate upstream credential flow

    Never pass the MCP client’s access token unchanged to an upstream API. That is forbidden token passthrough and creates confused-deputy risk.

    The MCP Security Best Practices dated 2025-11-25 defines token passthrough as accepting a client token without verifying that it was issued to the MCP server, then forwarding it to a downstream API. The practice can bypass controls that depend on the token audience, prevent the MCP server from distinguishing clients, distort downstream audit records, and let a stolen token be used through the server as a proxy.

    Replace code shaped like this:

    inbound = request.headers.Authorization
    upstreamRequest.headers.Authorization = inbound

    with two explicit operations:

    validate inbound token for the MCP server audience
    obtain the server's separately authorized upstream credential
    call the upstream API with that upstream credential

    The upstream credential must be issued for the upstream API, not for the MCP server. The MCP server is the OAuth client from the upstream API’s point of view. If the upstream authorization arrangement requires user consent, the proxy server must implement that flow and its required per-client controls; it cannot substitute the incoming MCP token.

    Separate credentials cost implementation work because the server must manage another authorization relationship. The alternative is not a simpler form of the same security model. It removes the recipient boundary, weakens attribution, and lets one service receive a bearer token intended for another.

    Also remove any fallback that forwards the inbound token when the upstream credential is absent. A fallback preserves the passthrough vulnerability precisely when configuration is incomplete. Fail the operation before calling the upstream API instead.

  8. Run negative tests across both boundaries

    Test failures before the successful path. A correct implementation is demonstrated by what it refuses, not just by one accepted token.

    Test Expected result
    Client omits resource from the authorization request Client implementation fails the MCP requirement; the request is not treated as a compliant flow
    Client omits resource from the token or refresh request Client implementation fails the MCP requirement; no unbound fallback is used
    resource is malformed or unknown to the authorization server Authorization server rejects it with invalid_target
    Token is valid but issued for a different MCP server Target MCP server returns HTTP 401 and performs no MCP operation
    Token for the upstream API is presented directly to the MCP server MCP server returns HTTP 401
    Valid token has insufficient scope MCP server returns HTTP 403 rather than reporting an audience failure
    Token is expired or otherwise invalid MCP server returns HTTP 401
    Upstream credential is unavailable MCP operation fails before an upstream call; the inbound token is not forwarded

    For each rejection, verify that no tool handler and no upstream request ran. Logging only the final status is insufficient to detect a handler that started before authorization failed.

    Inspect all grant paths, not just the interactive login: initial authorization, code exchange, refresh, step-up authorization, and reconnection after cached credentials expire. The resource value should remain the same for the same MCP server. If the client supports several servers, each server should receive a token issued for its own audience.

    Finally, inspect the server’s tool boundary separately from its token boundary. Audience validation determines whether the caller may address this MCP server; it does not decide which of the server’s tools should be available. Apply the authorization result alongside the narrower exposure rules described in restricting the tools an MCP server exposes.

Expected result

The MCP client sends the intended MCP server’s canonical URI as resource in every authorization, token, refresh, and reauthorization request. The authorization server issues an access token restricted to that audience. The MCP server accepts only tokens issued for its own audience, returns HTTP 401 for wrong-audience tokens, and never forwards the client’s token to an upstream API. Any upstream call uses a separate credential issued for that upstream resource.

Sources

  1. MCP Authorization specification dated 2025-11-25modelcontextprotocol.io
  2. RFC 8707 Resource Indicators for OAuth 2.0rfc-editor.org
  3. MCP Security Best Practices dated 2025-11-25modelcontextprotocol.io

See also