Token audience validation failed
The token is real — right issuer, good signature, not expired — but the `aud` claim names something other than this MCP server, so the server is required to reject it, and does.
- MCP Specification
- 2026-07-28
Is this you?
Do not start from your configuration. Read the two strings that are actually being compared — the resource identifier your server publishes, and the aud the authorization server put in the token — and diff them by eye. That settles which side to change, which is the whole question.
- This error
- Step 2 prints an
audthat is a GUID, anapi://…identifier,account, an identifier belonging to a sibling MCP route, or the same URL as step 1 with one extra trailing slash — and step 3 returns401withWWW-Authenticate: Bearer error="invalid_token". Noaudkey at all is the same failure with less to look at: an Amazon Cognito access token has none unless a resource binding was requested, and a token minted without any resource indicator often has none either. If step 2 fails to decode, the token is opaque rather than a JWT — the audience then lives in the introspection response, not in the token, and you need your server's introspection log instead. If step 1 404s or prints a URL you do not recognise, fix that first: an audience check against a resource identifier the server invented for itself will never pass. - Then the fix is the next section.
- Working
- Step 2's
audcontains step 1's string character for character — same scheme, same host, same path, same trailing slash or absence of one — and step 3 returns200. Anaudthat is an array is fine: RFC 9068 asks only that it contain a value the resource server expects for itself, so one matching entry ends the check. Anaudthat is a prefix of the URL you dialed can also be fine, because some servers match hierarchically — the MCP Python SDK's example verifier does, throughcheck_resource_allowed— but that is your server's choice, not a rule you can assume. - Then this is not your problem — try the error reference.
4 other strings clients print for this
- invalid audience
- Token audience validation failed - no matching audiences
- Token resource validation failed. Expected: <resource>
- The aud claim is not valid
Dotted spans such as <url> are placeholders — your client prints your own value there.
Fix it
Two strings have to match: the resource identifier your MCP server expects, and the aud your authorization server actually minted. Decode the token and compare them literally, trailing slash included, before changing anything. Then move whichever one is wrong. Most authorization servers do not put your MCP URL in aud by default — a Microsoft Entra ID v2.0 access token's aud is always the API's client ID, a Keycloak audience is a client ID, an Amazon Cognito access token has no aud at all unless the client asked for a resource binding, and Auth0 uses its own audience parameter unless you turn on its resource-parameter profile. Where the provider can be made to mint your canonical MCP URI, do that; where it cannot, configure your server to expect the identifier the provider does mint, one per MCP route. Turning the check off is not on the list: the 2026-07-28 spec makes it a **MUST**.
Have your agent fix this
Copies a prompt that sends your coding agent to this page, then has it find and apply the right fix for your project.
The cause depends on your platform. Pick yours.
Which side decides the audience?
Microsoft Entra ID
If the token came from Microsoft Entra ID and its aud is a GUID rather than your MCP URL
- 1 This is documented behaviour, not a misconfiguration. Microsoft's access token claims reference describes
audas "an Application ID URI or GUID", and is specific about the version split: "In v2.0 tokens, this value is always the client ID of the API. In v1.0 tokens, it can be the client ID or the resource URI used in the request." So a v2.0 access token will never carryhttps://api.example.com/mcpinaud, no matter whatresourcethe client sends. - 2 Which version you get is a property of the API, not of the client. The app manifest's
requestedAccessTokenVersion"specifies the access token version expected by the resource… Possible values… are 1, 2, or null. If the value is null, this parameter defaults to 1", and Microsoft notes that "the endpoint used, v1.0 or v2.0, is chosen by the client and only impacts the version of id_tokens". Check that attribute before you theorise about the client. - 3 The practical fix is to configure your MCP server to expect what Entra mints — the API app registration's client ID, or its Application ID URI — rather than to expect Entra to mint your canonical MCP URI. Microsoft's own guidance on the claim is the same instruction from the other side: "This value must be validated, reject the token if the value doesn't match the intended audience."
- 4 Do not expect the RFC 8707
resourceparameter to change the outcome at the v2.0 token endpoint. An open MCP Python SDK issue reports Entra rejecting it outright on refresh withAADSTS9010010("The resource parameter provided in the request doesn't match with the requested scopes"), and the same issue traces a plain audience mismatch to a trailing slash: Pydantic'sAnyHttpUrlrenders a bare-domain resource ashttps://mcp-server.example.com/, while the Entra app registration holdshttps://mcp-server.example.com. - 5 If you do want a URL-shaped audience, it has to be registered as an Application ID URI, and the format rules are strict: the supported forms include
api://<appId>andhttps://<verifiedCustomDomain>/<string>, "the recommendation is to use api://<appId>", and "the application ID URI value must not end with a slash". That last rule and the spec's preference for the no-trailing-slash form agree, which is convenient — pick one string and use it in both places.
Auth0
If the token came from Auth0 and aud is an API identifier you did not ask for, or the token will not decode at all
- 1 Auth0 has a documented answer for MCP specifically, and it is a tenant toggle. Its Auth for MCP guide states the mismatch plainly: "The Model Context Protocol (MCP) specification requires the use of the standards-compliant
resourceparameter as defined in RFC 8707. Auth0's Authentication API has historically used theaudienceparameter to specify a target resource server (API)." Until you change that, "the Resource Parameter Compatibility Profile will keep the experience as is and only use theaudienceparameter in the Auth0 access token" — your client'sresourceis dropped on the floor. - 2 Enable it in **Settings → Advanced**: **Resource Parameter Compatibility Profile** ("enables support for the
resourceparameter in authorization requests, allowing access tokens to be scoped to a specific API") and, in the same block, **Include Issuer in Authorization Responses** for the RFC 9207issparameter clients now validate. After that, "Auth0 will use theresourceparameter if it is available to define the token's audience." - 3 Know the precedence before you debug the result: "If both the
resourceandaudienceare available, theaudiencewill still be used." A client library that sets an Auth0audiencefor you will silently win over theresourcethe MCP spec requires. Supported flows forresourceare the standard authorization flow, PAR, JAR, CIBA and the refresh token grant. - 4 The value has to exist as an API first, and its shape matters. An API's **Identifier** "is set upon API creation and cannot be modified afterward", and Auth0's recommendation for MCP is explicit: "RFC 8707 requires the
resourceparameter to be an absolute URI, to conform with RFC 8707, we recommend defining your resource server identifiers (API identifiers) in URI format." Register the canonical MCP URI as the API identifier and the two strings become the same string. - 5 One caveat if you federate: with the profile enabled, "
resourceis not available as an upstream IdP parameter, and will not be forwarded", and Auth0 says "if passing the resource parameter to an upstream IdP, the Resource Parameter Compatibility Profile should not be enabled."
Keycloak
If the issuer is a Keycloak realm and aud is a client ID, account, or absent
- 1 Keycloak's audiences are client IDs, not URLs. Its audience-support documentation is direct about the model: "The claim
audshould typically represent client ids of all services where the token is supposed to be used." An MCP server expectinghttps://api.example.com/mcpwill therefore never match a default-configured realm token, and theresourceparameter your client sends does not change that. - 2 Understand why
audis sometimes empty. The defaultrolesclient scope carries an *Audience Resolve* mapper that "checks for clients that have at least one client role available for the current token. The client ID of each such client is then added as an audience" — so with no client roles assigned, no audience appears. And the client the token was issued *to* is never added: "the frontend client itself is not automatically added to the access token audience… since the access token will not contain the client for which the token is issued as an audience." - 3 Use the *Audience* mapper instead, which does not depend on roles. "A hardcoded audience is a protocol mapper, that will add the client ID of the specified service client as an audience to the token. You can use any custom value, for example a URL, if you want to use a different audience than the client ID." That last sentence is the fix: create the mapper with your canonical MCP URI as the custom audience.
- 4 Then decide how it is requested. Attach the mapper's client scope as **Optional** and the audience appears only when the client sends
scope=<that-scope>; attach it as **Default**, or put the mapper on the client's dedicated scope, and Keycloak will "always apply the audience for all the authentication request" — which is what you want, because an MCP client sendsresource, not a Keycloak-specific scope value. - 5 Keycloak's own setup checklist has two items, and the second is your server: "Ensure that services are configured to check audience on the access token sent to them. Ensure that access tokens issued by Keycloak contain all necessary audiences." Fixing only the realm side leaves the error in place if your verifier is comparing against the wrong string.
Amazon Cognito
If the issuer is an Amazon Cognito user pool and the token has no aud claim at all
- 1 A Cognito access token has no audience by default, and AWS documents that as the normal case. In the access token payload reference,
audis "the URL of the API that the access token is intended to authorize for. Present only if your application requested a resource binding from your authorization server." The claim that is always there isclient_id, "the user pool app client that authenticated your user" — which is not an audience and is not what your MCP server should be comparing against. - 2 Resource binding is Cognito's name for the thing the MCP spec requires: "With resource binding, also referred to as resource indicators, you can request API-specific grants from your user pool authorization server. Resource binding is an OAuth 2.0 extension defined in RFC 8707." When a client uses it, "Amazon Cognito sets the requested URI as the audience in the
audclaim of the access token." - 3 Turn it on the way AWS documents it: configure a resource server whose identifier is URL-formatted — "if the requested resource is a user pool resource server, the resource server identifier must be in a URL format" — then let the client include
&resource=https://api.example.com/mcpon the/oauth2/authorizerequest. "You can request one resource per authentication request", which suits one MCP route per token. - 4 Two limits decide whether this is available to you at all, and both are stated in the docs. "This feature is exclusive to managed login authentication with your user pool OAuth 2.0 authorization server. You can request resource binding in implicit and authorization-code grants from the Authorize endpoint… It is not currently available in SDK authentication models." And for machine callers: "You can only bind access tokens to resources for users. You can't request a resource binding with client-credentials M2M grants." An MCP server fronted by a client-credentials agent therefore has no
audto check on Cognito, and has to bind identity some other way. - 5 Refreshes are safe once the first token is right: "token-refresh grants from the Token endpoint carry over the
audclaim from the original request." So an audience that appears on login and vanishes an hour later is a different bug than this one.
FastMCP
If the value your own server expects is the thing that is wrong — a FastMCP JWTVerifier, or any verifier you configured by hand
- 1
audienceonJWTVerifieris a plain string comparison against the token's claim, and FastMCP describes what it buys you: "Theissuerparameter ensures tokens come from your trusted authentication system, whileaudiencevalidation prevents tokens intended for other services from being accepted by your MCP server", and "only tokens with the correct issuer and audience claims will be accepted." Set it to the string your provider mints, which thecurlabove already told you. - 2 Notice that FastMCP's own documented example uses
audience= "mcp-production-api"— an opaque label, not a URI. That is a perfectly good audience for an internal service and a poor one for an MCP server, because the spec requires clients to send an absolute canonical URI asresource. If you keep a label, keep it deliberately, and know that no client-sideresourcevalue will ever produce it. - 3 Omitting
audiencedoes not fail closed, it fails open: the parameter is simply absent from the comparison, and any token your issuer signed is accepted — including one minted for a sibling MCP route. That is the cross-service replay the spec's **MUST** exists to stop, so treat a missingaudienceas the more serious bug than the error you came here for. - 4 Check which verifier you are actually running before you look for the parameter. FastMCP's documented options for
IntrospectionTokenVerifierare the introspection URL, client credentials,required_scopes,client_auth_methodandhttp_client— no audience among them — so an opaque-token deployment needs the audience asserted from the introspection response by other means. AndDebugTokenVerifier"by default… accepts any non-empty token as valid", which is documented as development-only. - 5 The same reasoning applies one layer down in the reference SDKs, where there is no
audiencesetting to find: in the TypeScript SDK "verifyAccessTokenis the one function you supply", and in the Python SDK the audience check lives in theTokenVerifieryou write. The SDK's own example verifier does it withcheck_resource_allowedand logsToken resource validation failed. Expected: <resource>— which is this page's error under a different name.
Zuplo MCP Gateway
If you would rather not negotiate an audience shape with each identity provider one MCP route at a time
- 1 For its MCP routes the gateway is both the OAuth 2.1 resource server and the authorization server, so it mints the token whose audience it later checks. Per the auth overview, "every gateway-issued access token is bound to" two things: "the canonical resource URI of the MCP route the user authorized for, derived from the request origin and the route path", and "the
operationIdof the route". There is no provider-specific audience shape to reconcile, because the provider is not the one issuing the token the MCP client presents. - 2 The binding is enforced per route, not per project: "the gateway rejects a token presented at a different route or a different canonical resource. A token issued for
/mcp/linear-v1cannot be reused on/mcp/stripe-v1." That is the cross-route replay this error is supposed to prevent, made structural — the MCP OAuth policy "validates the gateway-issued bearer token, asserts audience binding and scope". - 3 Your identity provider stays where it is.
mcp-oauth-inbounddelegates browser login to an OIDC provider you configure, and the gateway issues its own token afterwards — so whether Entra mints a GUID or Keycloak mints a client ID stops being something every MCP client has to agree with. - 4 Two things to know before relying on it, both documented. The gateway's tokens are opaque, so there is no
audclaim for you to decode in thecurlabove — the binding is checked at the gateway, not readable from the token. And the canonical resource URI "is constructed from the incoming request origin", honouringHostthenX-Forwarded-Host: front it with a proxy that rewrites those and it will bind tokens to an origin the client never dialed, which is this same error one layer along.
Something else
Whatever the provider is, this error has exactly two levers, and the curl above tells you which one to pull. Change what the authorization server puts in aud, by registering your canonical MCP URI as the resource identifier and making sure the parameter that selects it — resource, or a vendor-specific audience — actually reaches the token endpoint. Or change what your server expects, to the identifier the provider does mint. Prefer the first: the 2026-07-28 spec requires clients to send the canonical MCP URI as resource on every authorization and token request, so a provider that honours it needs no per-client special-casing. Take the second when the provider cannot be moved — and then keep one distinct audience per MCP route, so the check still does the job it exists for. What is not a lever is switching the comparison off. Both the spec and RFC 9068 make audience validation a **MUST**, and a server that checks only the issuer accepts every token its identity provider ever signed, including the one minted for the route you did not want this caller to reach.
Why it happens
The 2026-07-28 spec puts this check on you and gives you no discretion: "MCP servers **MUST** validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2", and "MCP servers **MUST** only accept tokens specifically intended for themselves and **MUST** reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient of the token". RFC 9068 says the same thing for a JWT access token and names the wire error: the resource server "MUST validate that the aud claim contains a resource indicator value corresponding to an identifier the resource server expects for itself", and on failure the response "MUST include the error code invalid_token". So the rejection is correct behaviour. What is usually wrong is the value being compared. The spec tells clients what to ask for — the resource parameter "**MUST** be included in both authorization requests and token requests", "**MUST** use the canonical URI of the MCP server", and clients "**MUST** send this parameter regardless of whether authorization servers support it" — and that last clause is where this error lives. A client can send a perfect resource=https://api.example.com/mcp to an authorization server that ignores it, and get back a token whose aud is a GUID, a client ID, an API identifier you picked years ago, or nothing at all. Three other shapes produce the same string. The two values differ only by a trailing slash: an open MCP Python SDK issue traces exactly that to Pydantic's AnyHttpUrl appending one to a bare-domain resource, which then no longer matches the audience registered at the provider. The token is for a sibling route — the same issuer signs both, so only the audience check separates a public MCP route from an internal one. Or the server's expected audience is simply a leftover: a placeholder, a staging URL, or a value copied from a tutorial.
The exchange
Each step in the exchange, in order.
- MCP client
- Authorization server
- MCP server resource server
-
The client did what the spec requires — resource parameter, canonical URI, on both the authorization and the token request. Nothing obliges the authorization server to honour it, and this one did not.
MCP client to Authorization server
POST /token resource=https://api.example.com/mcp -
Authorization server to MCP client
200 access_token aud: "api://11112222-bbbb-3333" -
Everything about the token is valid except who it is for. RFC 9068 requires the rejection and requires the error code invalid_token, so a conformant server has no option to let it through.
MCP client to MCP server
POST /mcp Authorization: Bearer eyJhbGciOi… -
Inside MCP server
compare aud to own resource id https://api.example.com/mcp -
MCP server to MCP client
401 Unauthorized Bearer error="invalid_token"Flow stops here
What lands on you next
One claim, two strings, and an afternoon spent proving they were different. The token that finally gets in raises a different set of questions.
-
The audience is right. May this user call this particular tool?
-
require-user-claims-inboundA correct audience says the token is for us, not that its subject is entitled. Required claims are asserted per request, per user, before the call reaches your server.
-
Which tools does this route expose in the first place?
-
mcp-capability-filter-inboundA static per-route allow-list, with no per-user axis: capabilities outside it are hidden from
tools/listand blocked withMethodNotFound. -
The caller is authenticated. What does the upstream API receive?
-
mcp-token-exchange-inboundNot this token: the spec forbids passthrough with a **MUST NOT**. The inbound credential is dropped and an independent upstream one is resolved per user.
-
We rejected a token. Where does that show up an hour later?
-
mcp_request_rejectedA rejected MCP request emits its own analytics event, so a wave of audience failures is something you can watch rather than something a user has to report.
All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.
Stop debugging auth on every MCP server
A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.