Mint per-resource MCP audiences via optional client scopes
Date: 2026-07-03 Status: Accepted Decision: hl-sqk9 Deciders: Sean Brandt
Context
Section titled “Context”Keycloak 26 validates the RFC 8707 resource= parameter but never maps it to the access-token aud claim; only Audience protocol mappers (and the Audience Resolve mapper) stamp audiences. The existing blanket audience mapper on the built-in basic realm-default client scope stamps aud=mcp-public on every client — including RFC 7591 dynamically-registered (DCR) MCP clients — so the k8s API cannot distinguish a k8s-intended token from any other MCP token. This decision fixes the mechanism for producing a resource-scoped audience that DCR clients can obtain without per-client Keycloak configuration.
Decision
Section titled “Decision”Advertise a resource-named optional client scope carrying an Audience protocol mapper in the route’s RFC 9728 resourceMetadata.scopesSupported. A DCR client must request that scope (the MCP authorization spec obligates clients to forward all advertised scopesSupported) to receive that resource’s audience. The scope is registered as a realm optional scope, which DCR-registered clients inherit automatically.
Rationale
Section titled “Rationale”- Keycloak 26’s native RFC 8707
resource=handling only validates the parameter; it is not an audience-stamping mechanism (verified via deepwiki againstkeycloak/keycloak). - MCP’s mandatory
scopesSupported-forwarding makes an optional-scope-gated audience the only DCR-compatible, least-privilege path — the audience appears only when the resource scope is requested, so it fails closed. - This is the pattern Keycloak’s own MCP guide documents (
docs/guides/securing-apps/mcp-authz-server.adoc).
Alternatives Considered
Section titled “Alternatives Considered”- Realm optional client scope + Audience mapper advertised via
scopesSupported(chosen): DCR clients inherit realm optional scopes automatically; MCP clients forward advertised scopes; the mapper fires (and the audience appears) only when the scope is requested, so it fails closed. - Enable Keycloak’s native RFC 8707
RESOURCE_INDICATORSfeature (rejected): only validatesresource=, never stampsaudfrom it; addsINVALID_TARGETfriction with no audience benefit. - Extend the existing blanket
mcp-publicmapper to also stamp the k8s audience (rejected): every realm token would gain trust to the k8s API — the exact least-privilege violation this design exists to fix.
Consequences
Section titled “Consequences”- Positive: audiences are minted only when the resource scope is requested (fails closed); the pattern is reusable for Phase B (engram, hl-u18e) and Phase C (blanket-mapper removal, hl-05xz).
- Negative: a per-resource Terraform client scope + Audience mapper is required for every MCP route that needs a scoped audience.
- Neutral: the blanket
mcp-publicmapper is left untouched in Phase A.