feat: implement RFC 8693 Standard Token Exchange for Keycloak
Configure Keycloak 26.4.2 realm to support Standard Token Exchange V2, enabling the MCP server to exchange client tokens (aud: nextcloud-mcp-server) for Nextcloud-scoped tokens (aud: nextcloud) via RFC 8693. Changes: - Remove duplicate audience workarounds from realm configuration - Add token-exchange-nextcloud client scope with audience mapper - Configure scope as default for nextcloud-mcp-server client - Enable standard.token.exchange.enabled on both clients - Add comprehensive integration tests (7 tests, all passing) Token Exchange Flow: 1. Client obtains token with aud: [nextcloud-mcp-server, nextcloud] 2. Server exchanges to aud: nextcloud, azp: nextcloud-mcp-server 3. Exchanged token used for Nextcloud API calls 4. Each request gets fresh ephemeral token (stateless) Key Implementation Details: - Uses Keycloak 26.2+ scope-based authorization (no FGAP required) - Target audiences must be in client's default/optional scopes - Protocol mappers alone don't grant exchange permission - Tokens expire after 300s (5 minutes) Tests validate: - Basic token exchange flow - Nextcloud API integration (Capabilities, Notes) - CRUD operations with exchanged tokens - Multiple stateless exchanges from same client token - Token claims preservation (aud, azp, sub) - Scope configuration validation See docs/ADR-004-progressive-consent.md for architecture details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+18
-24
@@ -166,13 +166,13 @@
|
||||
{
|
||||
"clientId": "nextcloud",
|
||||
"name": "Nextcloud Resource Server",
|
||||
"description": "Resource server for Nextcloud APIs - used by user_oidc app for bearer token validation",
|
||||
"description": "Resource server for Nextcloud APIs - used by user_oidc app for bearer token validation and as token exchange target",
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "nextcloud-secret-change-in-production",
|
||||
"redirectUris": [],
|
||||
"webOrigins": [],
|
||||
"bearerOnly": true,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
"standardFlowEnabled": false,
|
||||
"implicitFlowEnabled": false,
|
||||
@@ -181,7 +181,10 @@
|
||||
"publicClient": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"display.on.consent.screen": "false"
|
||||
"display.on.consent.screen": "false",
|
||||
"token.exchange.grant.enabled": "true",
|
||||
"client.token.exchange.standard.enabled": "true",
|
||||
"standard.token.exchange.enabled": "true"
|
||||
},
|
||||
"fullScopeAllowed": true,
|
||||
"nodeReRegistrationTimeout": -1
|
||||
@@ -220,18 +223,19 @@
|
||||
"client_credentials.use_refresh_token": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"token.exchange.grant.enabled": "true",
|
||||
"client.token.exchange.standard.enabled": "true"
|
||||
"client.token.exchange.standard.enabled": "true",
|
||||
"standard.token.exchange.enabled": "true"
|
||||
},
|
||||
"fullScopeAllowed": true,
|
||||
"nodeReRegistrationTimeout": -1,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "audience-nextcloud",
|
||||
"name": "audience-mcp-server",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"included.custom.audience": "nextcloud",
|
||||
"included.custom.audience": "nextcloud-mcp-server",
|
||||
"access.token.claim": "true",
|
||||
"id.token.claim": "false"
|
||||
}
|
||||
@@ -308,13 +312,15 @@
|
||||
"web-origins",
|
||||
"profile",
|
||||
"roles",
|
||||
"email"
|
||||
"email",
|
||||
"token-exchange-nextcloud"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"offline_access",
|
||||
"microprofile-jwt",
|
||||
"token-exchange-nextcloud",
|
||||
"notes:read",
|
||||
"notes:write",
|
||||
"calendar:read",
|
||||
@@ -685,27 +691,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "audience",
|
||||
"description": "Audience scope for token validation",
|
||||
"name": "token-exchange-nextcloud",
|
||||
"description": "Allows token exchange for nextcloud client",
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"include.in.token.scope": "true",
|
||||
"include.in.token.scope": "false",
|
||||
"display.on.consent.screen": "false"
|
||||
},
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "mcp-server-audience",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"included.client.audience": "nextcloud-mcp-server",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nextcloud-audience",
|
||||
"name": "nextcloud-audience-for-exchange",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-mapper",
|
||||
"consentRequired": false,
|
||||
@@ -756,8 +751,7 @@
|
||||
"profile",
|
||||
"email",
|
||||
"roles",
|
||||
"web-origins",
|
||||
"audience"
|
||||
"web-origins"
|
||||
],
|
||||
"defaultOptionalClientScopes": [
|
||||
"offline_access",
|
||||
|
||||
Reference in New Issue
Block a user