patchbay audit
·4 min read

Audience validation for MCP servers: a token for another API should not work on yours

Your MCP server checks that every request carries a valid bearer token: the signature verifies, the issuer is your authorization server, the token has not expired. That sounds complete. It leaves one question unasked: was this token issued for this server?

If the answer is never checked, a token issued for any other service behind the same authorization server works on your MCP server too.

The setup that makes this matter

One authorization server usually issues tokens for several resources. A company runs an internal API, a public API, a billing service and more recently an MCP server, all trusting the same issuer. Every token carries an aud (audience) claim naming the resource it was meant for:

{
  "iss": "https://auth.example.com",
  "aud": "https://billing.example.com",
  "sub": "user-4821",
  "exp": 1794400000
}

That token is genuine. Its signature is valid and its issuer is the one your MCP server trusts. If the MCP server does not compare aud against its own identifier, it accepts the token and the holder can call your tools with a credential that was only ever meant for billing.

The practical consequence is that your MCP server is as hard to get into as the least protected service sharing its issuer. A token leaked from a log file of a low-value service, or obtained by a client that was only granted access to that service, becomes a key to an endpoint that can run tools.

What the specs require

RFC 8707 (Resource Indicators) lets a client name the resource it wants a token for, so the authorization server can set the audience accordingly. RFC 9068, the JWT profile for access tokens, requires the resource server to check that aud contains its own identifier and to reject the token if it does not. The MCP authorization spec makes both mandatory from revision 2025-06-18: clients must send the resource parameter and servers must only accept tokens issued for themselves. The earlier 2025-03-26 revision does not mention audience, so a server built against it may never have added the check.

Why metadata does not tell you

There is no standard metadata field that says a server validates the audience. RFC 8707 defines the resource request parameter and nothing a server could publish about it. Some servers put a resource_indicators_supported flag in their metadata, but no specification defines that flag and it says nothing about what happens at runtime. The only way to know is to present a token with the wrong audience and see what comes back.

Test it

patchbay audit does this as an opt-in probe. You supply a token that is valid on the same authorization server and was issued for a different resource:

npx -y @patchbay-nebius/agent audit https://mcp.example.com/mcp --probe-audience=<token>

The probe sends tools/list with that token. A 401 or 403 means the server rejected it and the check passes. If the request succeeds you get AUDIENCE-001 at critical severity. Anything else is reported as inconclusive. The token can also be passed through the PATCHBAY_PROBE_AUDIENCE_TOKEN environment variable, which keeps it out of shell history and CI logs.

Because the probe needs a token you are entitled to hold, it is not part of public sweeps and there is no ecosystem-wide number for this finding. Run it against servers you operate.

The fix

  1. Decide on one canonical resource identifier for the MCP server (normally its public URL) and publish it as resource in /.well-known/oauth-protected-resource.
  2. On every request, compare the token's aud claim with that identifier. aud may be a string or an array; accept the token only if your identifier is in it.
  3. Reject everything else with HTTP 401. Do not fall back to accepting tokens that have no aud at all.
  4. If you use opaque tokens, the same rule applies to the audience returned by token introspection.

Most JWT libraries take the expected audience as a verification option and do nothing with the claim unless you pass it. It is worth checking that the option is set in the code path your MCP endpoint actually uses.