Why Keycloak Breaks MCP Servers (the Audience Claim) and How RFC 8707 Fixes It

Guilliano Molaire Guilliano Molaire 6 min read

Last updated: May 2026

If your MCP client keeps getting a 401 from a Keycloak-protected MCP server, the culprit is almost always the token’s aud (audience) claim. The MCP authorization spec expects the client to ask for a token scoped to a specific resource, but older Keycloak versions ignored that request and minted a token with the wrong audience, so the MCP server rejected it.

This post walks through the exact symptom, the root cause in RFC 8707 terms, a copy-pasteable fix you can ship today, and what is changing in Keycloak core so the workaround eventually disappears.

The symptom: a clean 401 with a WWW-Authenticate challenge

You wire up an MCP server behind Keycloak. The OAuth dance looks healthy: the client discovers the authorization server, redirects, gets a code, exchanges it, and receives a perfectly valid-looking access token. Then the first real MCP call comes back like this:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
  error_description="Token audience does not match this resource server"

The token is not expired. The signature verifies. Scopes look fine. The MCP server still says no.

What is happening: the MCP server is a resource server, and a well-behaved resource server checks that the token was actually issued for it. That check reads the aud claim. When aud does not name this MCP server, the only correct response is 401 plus a WWW-Authenticate: Bearer challenge. The client did everything right; the token just was not minted for this resource.

The root cause: RFC 8707 resource indicators and the aud claim

The MCP authorization spec leans on RFC 8707 Resource Indicators for OAuth 2.0. The idea is simple and important for an agent ecosystem where one client may hold tokens for many tools:

  • When the client requests a token, it sends a resource parameter naming the MCP server it intends to call (for example resource=https://mcp.example.com).
  • The authorization server is supposed to honor that parameter and stamp the issued token’s aud claim with that resource.
  • The MCP server then accepts the token only if its own identifier appears in aud.

This is what stops a token minted for Tool A from being silently replayed against Tool B. The aud claim is the boundary.

Here is the breakdown: Keycloak historically ignored the resource parameter. A client could send resource=https://mcp.example.com all day long, and Keycloak would issue a token whose aud was driven by its own client and scope configuration, not by the request. The result was a token with a wrong or missing audience. The MCP server compared aud against itself, found no match, and returned 401. From the client’s side everything looked correct, which is exactly why this bug is so confusing to chase down.

Keycloak’s own documentation has described its MCP authorization support as partial, and the audience handling is the sharp edge most teams hit first.

How to fix it today: force the audience with a protocol mapper

On Keycloak versions that do not yet honor resource natively, you make Keycloak put the right value in aud yourself, using an Audience protocol mapper attached to a dedicated client scope. You are not relying on the resource parameter at all; you are hardcoding the audience for tokens issued to this client.

The cleanest pattern is one client scope per MCP server, added as a default scope on the OAuth client your agent uses.

1. Create a client scope with an audience mapper

# Create a client scope for the MCP server
kcadm.sh create client-scopes -r my-realm -b '{
  "name": "mcp-audience",
  "protocol": "openid-connect",
  "attributes": { "include.in.token.scope": "true" }
}'

# Attach an Audience mapper that stamps aud with the MCP server identifier
kcadm.sh create "client-scopes/<SCOPE_ID>/protocol-mappers/models" -r my-realm -b '{
  "name": "mcp-aud",
  "protocol": "openid-connect",
  "protocolMapper": "oidc-audience-mapper",
  "config": {
    "included.custom.audience": "https://mcp.example.com",
    "access.token.claim": "true",
    "id.token.claim": "false"
  }
}'

The key field is included.custom.audience: set it to the exact identifier your MCP server checks for, byte for byte. A trailing slash or http vs https mismatch will keep the 401 coming.

2. Make the scope a default on your MCP client

kcadm.sh update "clients/<CLIENT_ID>/default-client-scopes/<SCOPE_ID>" -r my-realm

3. Verify the claim

Decode the issued access token (paste the middle segment into any JWT decoder) and confirm the audience is present:

{
  "iss": "https://keycloak.example.com/realms/my-realm",
  "aud": "https://mcp.example.com",
  "scope": "openid mcp-audience",
  "exp": 1735689600
}

Once aud names the MCP server, the resource server’s check passes and the 401 disappears.

If you are on Keycloak 26.4 or later, you have a head start: the RFC 8414 authorization-server metadata well-known endpoint shipped in 26.4, so MCP clients can discover your Keycloak automatically instead of being hand-fed endpoints. The audience mapper above is still the reliable way to control aud until native resource-indicator support is fully in a stable release. Treat the mapper as the belt and the native support as the suspenders.

What is changing in core

The manual workaround is collapsing into core configuration. As of 2026-05-29:

  • Native RFC 8707 Resource Indicators is tracked in keycloak/keycloak#14355, open since 2022 and milestoned for 26.7.0. This is the work that teaches Keycloak to honor the resource parameter and set aud from it, the way the MCP spec assumes.
  • Experimental native support has already landed via keycloak/keycloak#46763 (now merged), so the behavior is testable ahead of the milestone release.
  • The RFC 8414 well-known endpoint shipped in 26.4, and the current release at time of writing is 26.6.2.

So the trajectory is clear: the protocol-mapper trick is a bridge. Once you are on a release with resource-indicator support, you can let clients drive the audience with the resource parameter and retire the per-server audience mappers, or keep them as an explicit allowlist if you prefer tighter control.

Let Skycloak handle the audience plumbing

If you would rather not hand-roll audience mappers and track Keycloak milestones, that is what we do. Skycloak runs managed Keycloak with current versions, sensible OAuth defaults, and the metadata endpoints MCP clients expect, so protecting an MCP server is configuration, not a multi-day debugging session into why a valid token gets a 401. You bring the MCP server; we keep the identity layer correct as the spec and Keycloak evolve.

Start a free trial or talk to us about securing agent and MCP workloads.

FAQ

Why does my MCP client get a 401 even though the access token is valid?
The token is structurally valid but its aud claim does not name your MCP server. A resource server must reject tokens that were not issued for it, so it returns 401 with a WWW-Authenticate: Bearer challenge. Fix the audience, not the token lifetime or signature.

What is the resource parameter and why does it matter for MCP?
It is the RFC 8707 resource indicator. The MCP client sends resource=<your MCP server> so the authorization server can mint a token scoped to exactly that server. It is how one agent safely holds tokens for many tools without one tool’s token working against another.

Does Keycloak support RFC 8707 resource indicators?
Natively, not yet in a stable release. Support is tracked in issue #14355, milestoned for 26.7.0, with experimental code merged via PR #46763. Until then, use an Audience protocol mapper to set aud explicitly. Keycloak’s docs currently describe MCP authorization support as partial.

Do I need to upgrade Keycloak to fix the 401 today?
No. The audience-mapper workaround works on current and older versions. Upgrading to 26.4+ adds the RFC 8414 discovery endpoint so MCP clients can find your server automatically, and 26.7.0 is slated to honor the resource parameter natively, which lets you eventually drop the manual mappers.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman