Keycloak¶
Keycloak fronts an MCP server well, because it supports everything the SDK's client discovers on its own:
authorization-server metadata, anonymous dynamic client registration, and standard scope claims.
Configure the realm¶
- Create a realm (
mcpbelow). Its issuer ishttps://kc.example.com/realms/mcp. - Enable Client registration > Anonymous access policies if MCP clients should register themselves. Skip this to require pre-registered clients instead.
- Define a client scope (
mcp:usebelow). Through a mapper, stamp the MCP server's canonical URI into the token'saudclaim, so the audience binding below has something to bind.
Validate the tokens¶
Validate the RS256 tokens with the shipped JwksAccessTokenValidator against the
realm's JWKS, published at /realms/mcp/protocol/openid-connect/certs:
use Firebase\JWT\CachedKeySet;
use Nexus\Mcp\Server\Auth\JwksAccessTokenValidator;
$validator = new JwksAccessTokenValidator(
new CachedKeySet(
'https://kc.example.com/realms/mcp/protocol/openid-connect/certs',
$httpClient,
$requestFactory,
$cache,
300,
rateLimit: true,
),
'https://kc.example.com/realms/mcp', // the realm issuer
'https://mcp.example.com/mcp', // this server's canonical URI
);
Mount it exactly as Validating tokens shows, and publish a
protected resource metadata document that names the realm issuer
under authorization_servers. The SDK client then walks discovery, registration, PKCE, and the token exchange
without Keycloak-specific configuration.
Quirks¶
- Keycloak identifies the authorizing client in
azp, notclient_id. The shipped validator reads both. - A token's
auddefaults toaccountunless a mapper adds your resource URI. Without the mapper,BearerAuthenticationMiddlewarerefuses every token, which is the audience binding doing its job.
All of this exists as a runnable whole in the Keycloak end-to-end example: a realm export with the scope, mapper, and registration policies configured, a compose file that imports it, and the protected server plus the flow-walking client.