Teams running more than one MCP server behind a single authorization server.
How to make an MCP access token unusable anywhere except the endpoint it was issued for.
The control gap
Once an organisation hosts several MCP servers, one authorization server usually issues tokens for all of them. Without an audience, a token obtained for a low-risk read-only server is a valid bearer token at the server that can move money. The user consented once, to something narrow, and the credential silently generalised.
What good looks like
Each token names exactly one resource. Presenting it to a different MCP endpoint fails at the audience check, before any tool is resolved and before any policy is consulted.
- Send resource on the authorization request and again on the token request, with the same canonical value both times.
- Reject a token request whose resource does not match the resource recorded in the authorization code, rather than quietly issuing a token for the new value.
- Compare the token audience against the endpoint's own canonical resource on every request, not only at issuance.
- Refuse resource values that carry a fragment, a foreign origin, or a path that does not resolve to a gateway the workspace actually owns.
A production workflow
- The client reads the canonical resource identifier from the protected resource metadata document.
- It includes that value as the resource parameter in the authorization request, and the server stores it with the pending authorization.
- The token request repeats the resource value, and the token endpoint refuses any mismatch.
- The MCP endpoint validates the token's audience against its own resource identifier and rejects anything issued for a sibling server.
Copy this
The resource value appears twice and must be identical both times. Endram validates that the requested resource resolves to a gateway on this origin and belongs to the consenting user's workspace before it issues a code.
# 1. Authorization request
GET /oauth/authorize
?response_type=code
&client_id=tfc_...
&redirect_uri=http://127.0.0.1:33418/callback
&scope=mcp:connect%20tools:read%20tools:call
&code_challenge=<S256 challenge>
&code_challenge_method=S256
&state=<opaque>
&resource=https%3A%2F%2Fendram.com%2Fmcp%2Fbilling-tools
# 2. Token request, same resource value
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code>
&redirect_uri=http://127.0.0.1:33418/callback
&client_id=tfc_...
&code_verifier=<verifier>
&resource=https%3A%2F%2Fendram.com%2Fmcp%2Fbilling-tools
# 3. What the resource server checks on every call
audience(token) == "https://endram.com/mcp/billing-tools" -> continue to policy
audience(token) != this endpoint -> 401 invalid_tokenThe check in step three is the one teams skip. Sending the parameter without enforcing the audience produces tokens that look correctly scoped in a decoder and behave like universal bearer credentials in production.
Evidence to require
- The resource value recorded on the authorization request, the code, and the issued token.
- Rejected token requests where the resource changed between authorization and exchange.
- Cross-resource presentation attempts: a token for one gateway offered to another.
- The consent record that authorised this resource, its scope, and its expiry.
Buyer checklist
- Does the token endpoint refuse a resource value that differs from the one in the authorization request?
- Is the audience checked on every tool call or only when the session is established?
- Can a user consent once and end up with authority over more than one MCP server?
- Are refresh tokens bound to the same resource as the access token they replace?
Practical answers
Common implementation questions
Is the resource parameter the same as an OAuth scope?
No. Scope says which operations are permitted. The resource indicator says which server may accept the token at all. A token can carry the right scope and still be invalid because it names a different resource.
What if a client does not send the resource parameter?
Decide deliberately. Endram treats a missing or non-matching resource as a failure rather than defaulting to a broad audience, because the default is the exact behaviour resource indicators exist to remove.
Does this replace per-tool authorization?
No. Audience binding decides which server the credential belongs to. Deciding whether this agent, acting for this user, may run this specific tool against this specific record is a separate evaluation that happens after the token is accepted.