---
title: "Token Audience Validation Failed: The aud Claim Names Another Server"
description: "Your MCP server rejects a valid, signed token because its aud claim names something else. What each authorization server puts in aud, and how to match."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/token-audience-validation-failed"
pageType: "mcp-error"
errorString: "Token audience validation failed"
lastVerified: "2026-07-30"
specVersion: "2026-07-28"
---

# `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.

```bash
# 1. The resource identifier your server expects. This is the string its
#    audience check compares against.
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource/mcp \
  | jq -r .resource
# → https://your-server.example.com/mcp

# 2. The audience the authorization server actually minted. Decode only —
#    no signature check, so never trust the output for anything but reading.
TOKEN='eyJhbGciOiJSUzI1NiIs...'
python3 -c 'import base64,json,sys
p = sys.argv[1].split(".")[1]
print(json.dumps(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))))' \
  "$TOKEN" | jq '{aud, iss, azp, client_id, scp, scope}'

# 3. Present it and read the challenge verbatim.
curl -sS -D - -o /dev/null -X POST https://your-server.example.com/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

**This error** — Step 2 prints an `aud` that is a GUID, an `api://…` 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 returns `401` with `WWW-Authenticate: Bearer error="invalid_token"`. No `aud` key 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.

**Working** — Step 2's `aud` contains step 1's string character for character — same scheme, same host, same path, same trailing slash or absence of one — and step 3 returns `200`. An `aud` that 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. An `aud` that 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, through `check_resource_allowed` — but that is your server's choice, not a rule you can assume. If this is what you see, this is not your problem.

**Clients also report this as:**

- `invalid audience`
- `Token audience validation failed - no matching audiences`
- `Token resource validation failed. Expected: <resource>`
- `The aud claim is not valid`

## Fix it

**In short.** 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**.

The symptom is identical across platforms; the cause is not.

### 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 `aud` as "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 carry `https://api.example.com/mcp` in `aud`, no matter what `resource` the 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 `resource` parameter to change the outcome at the v2.0 token endpoint. An open MCP Python SDK issue reports Entra rejecting it outright on refresh with `AADSTS9010010` ("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's `AnyHttpUrl` renders a bare-domain resource as `https://mcp-server.example.com/`, while the Entra app registration holds `https://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>` and `https://<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.

```json
// A decoded Entra v2.0 access token. Nothing here is wrong; the aud is
// the API app registration's client ID, which is what v2.0 always emits.
{
  "aud": "11112222-bbbb-3333-cccc-4444dddd5555",
  "iss": "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0",
  "azp": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "scp": "mcp",
  "ver": "2.0"
}
```

[Entra ID: access token claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference)

### 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 `resource` parameter as defined in RFC 8707. Auth0's Authentication API has historically used the `audience` parameter 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 the `audience` parameter in the Auth0 access token" — your client's `resource` is dropped on the floor.
2. Enable it in **Settings → Advanced**: **Resource Parameter Compatibility Profile** ("enables support for the `resource` parameter 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 9207 `iss` parameter clients now validate. After that, "Auth0 will use the `resource` parameter if it is available to define the token's audience."
3. Know the precedence before you debug the result: "If both the `resource` and `audience` are available, the `audience` will still be used." A client library that sets an Auth0 `audience` for you will silently win over the `resource` the MCP spec requires. Supported flows for `resource` are 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 `resource` parameter 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, "`resource` is 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."

```json
// What a decoded Auth0 access token should look like once the API
// identifier is the canonical MCP URI and the resource profile is on.
{
  "iss": "https://your-tenant.us.auth0.com/",
  "aud": "https://api.example.com/mcp",
  "azp": "aBcD1234...",
  "scope": "mcp",
  "sub": "auth0|68c1..."
}
```

[Auth0: Resource Parameter Compatibility Profile](https://auth0.com/ai/docs/mcp/guides/resource-param-compatibility-profile)

### 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 `aud` should typically represent client ids of all services where the token is supposed to be used." An MCP server expecting `https://api.example.com/mcp` will therefore never match a default-configured realm token, and the `resource` parameter your client sends does not change that.
2. Understand why `aud` is sometimes empty. The default `roles` client 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 sends `resource`, 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.

[Keycloak: Audience Support](https://www.keycloak.org/docs/latest/server_admin/index.html#audience-support)

### 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, `aud` is "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 is `client_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 `aud` claim 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/mcp` on the `/oauth2/authorize` request. "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 `aud` to 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 `aud` claim from the original request." So an audience that appears on login and vanishes an hour later is a different bug than this one.

```text
https://<your-domain>.auth.us-east-1.amazoncognito.com/oauth2/authorize
  ?response_type=code
  &client_id=1example23456789
  &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
  &scope=openid+mcp
  &code_challenge_method=S256
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &resource=https%3A%2F%2Fapi.example.com%2Fmcp
```

[Amazon Cognito: Resource binding](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html#cognito-user-pools-resource-binding)

### 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. `audience` on `JWTVerifier` is a plain string comparison against the token's claim, and FastMCP describes what it buys you: "The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation 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 the `curl` above 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 as `resource`. If you keep a label, keep it deliberately, and know that no client-side `resource` value will ever produce it.
3. Omitting `audience` does 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 missing `audience` as 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 `IntrospectionTokenVerifier` are the introspection URL, client credentials, `required_scopes`, `client_auth_method` and `http_client` — no audience among them — so an opaque-token deployment needs the audience asserted from the introspection response by other means. And `DebugTokenVerifier` "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 `audience` setting to find: in the TypeScript SDK "`verifyAccessToken` is the one function you supply", and in the Python SDK the audience check lives in the `TokenVerifier` you write. The SDK's own example verifier does it with `check_resource_allowed` and logs `Token resource validation failed. Expected: <resource>` — which is this page's error under a different name.

```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

verifier = JWTVerifier(
    jwks_uri="https://auth.example.com/.well-known/jwks.json",
    issuer="https://auth.example.com",
    # The aud your provider actually mints — copy it from the decoded
    # token, not from the URL you wish it were. Omit this and every token
    # this issuer signs is accepted, including tokens for other routes.
    audience="https://api.example.com/mcp",
)

mcp = FastMCP(name="Protected API", auth=verifier)
```

[FastMCP: Token Verification](https://gofastmcp.com/servers/auth/token-verification)

### 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 `operationId` of 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-v1` cannot 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-inbound` delegates 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 `aud` claim for you to decode in the `curl` above — the binding is checked at the gateway, not readable from the token. And the canonical resource URI "is constructed from the incoming request origin", honouring `Host` then `X-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.

[Zuplo MCP Gateway: authorization](/docs/mcp-gateway/auth/overview)

### 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

The MCP client asks the authorization server for a token, sending the RFC 8707 resource parameter naming the MCP server. The authorization server ignores that parameter and mints a token whose aud claim is its own API identifier instead. The client presents the token, the MCP server compares the aud claim against the resource identifier it publishes, they do not match, and the request is refused with 401 invalid_token.

**Participants:** MCP client, Authorization server, MCP server (resource server)

1. **MCP client → Authorization server** — `POST /token` — `resource=https://api.example.com/mcp`
2. **Authorization server → MCP client** — `200 access_token` — `aud: "api://11112222-bbbb-3333"` _(response; carries the credential)_
3. **MCP client → MCP server** — `POST /mcp` — `Authorization: Bearer eyJhbGciOi…` _(carries the credential)_
4. **MCP server** (internal step) — compare aud to own resource id — `https://api.example.com/mcp`
5. **MCP server → MCP client** — `401 Unauthorized` — `Bearer error="invalid_token"` _(response; this step fails — the flow stops here)_

Notes:

- **Steps 1–2** — 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.
- **Steps 3–5** — 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.

## 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-inbound` — A 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-inbound` — A static per-route allow-list, with no per-user axis: capabilities outside it are hidden from `tools/list` and blocked with `MethodNotFound`.
- **The caller is authenticated. What does the upstream API receive?** — `mcp-token-exchange-inbound` — Not 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_rejected` — A 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. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [OAuth 2.1 Authentication for MCP Servers](/learn/mcp/authentication/oauth-2-1) — Servers that must be reachable by MCP clients you do not control, or listed in a connector directory
- **Similar error** — [`Failed to fetch authorization server metadata from all attempted URLs`](/learn/mcp/errors/failed-to-fetch-authorization-server-metadata) — The client knows which authorization server it is supposed to use, but none of the well-known URLs it is required to try returned a metadata document — so it never learns where `/authorize` and `/token` are, and the flow stops one step short of a login.
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- **Similar error** — [`Authorization with the MCP server failed`](/learn/mcp/errors/oauth-completes-but-no-token-sent) — A token was issued and the client says it is connected, but the credential never reaches your server intact: either the client does not attach it, or it carries an audience your server is right to reject.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
