Skip to content

Mint per-resource MCP audiences via optional client scopes

Date: 2026-07-03 Status: Accepted Decision: hl-sqk9 Deciders: Sean Brandt

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.

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.

  • Keycloak 26’s native RFC 8707 resource= handling only validates the parameter; it is not an audience-stamping mechanism (verified via deepwiki against keycloak/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).
  • 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_INDICATORS feature (rejected): only validates resource=, never stamps aud from it; adds INVALID_TARGET friction with no audience benefit.
  • Extend the existing blanket mcp-public mapper 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.
  • 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-public mapper is left untouched in Phase A.