feat: enable authorization services for token exchange in Keycloak
Configure Keycloak authorization policies to allow nextcloud-mcp-server to exchange tokens for nextcloud audience. This enables RFC 8693 token exchange flow between the MCP client and Nextcloud. Changes: - Enable service accounts and authorization services for nextcloud client - Add token-exchange resource with scope-based permissions - Create client policy allowing nextcloud-mcp-server and nextcloud - Add token-exchange-permission with affirmative decision strategy - Add mcp-server-audience mapper to nextcloud-mcp-server client - Simplify audience validation to accept tokens with MCP client ID The authorization policy enables tokens issued to nextcloud-mcp-server to be exchanged for tokens with nextcloud audience, validated via both clients being included in the allow-nextcloud-mcp-server-to-exchange policy. All 7 token exchange integration tests pass, confirming: - Basic token exchange with correct audience claims - Nextcloud API access with exchanged tokens - Stateless multiple exchange operations - Full CRUD operations on Notes API - Proper claim preservation (sub, azp, aud) - Default scope configuration - TokenExchangeService implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -177,7 +177,8 @@
|
|||||||
"standardFlowEnabled": false,
|
"standardFlowEnabled": false,
|
||||||
"implicitFlowEnabled": false,
|
"implicitFlowEnabled": false,
|
||||||
"directAccessGrantsEnabled": false,
|
"directAccessGrantsEnabled": false,
|
||||||
"serviceAccountsEnabled": false,
|
"serviceAccountsEnabled": true,
|
||||||
|
"authorizationServicesEnabled": true,
|
||||||
"publicClient": false,
|
"publicClient": false,
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@@ -186,6 +187,56 @@
|
|||||||
"client.token.exchange.standard.enabled": "true",
|
"client.token.exchange.standard.enabled": "true",
|
||||||
"standard.token.exchange.enabled": "true"
|
"standard.token.exchange.enabled": "true"
|
||||||
},
|
},
|
||||||
|
"authorizationSettings": {
|
||||||
|
"allowRemoteResourceManagement": true,
|
||||||
|
"policyEnforcementMode": "ENFORCING",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "token-exchange",
|
||||||
|
"type": "urn:keycloak:token-exchange",
|
||||||
|
"ownerManagedAccess": false,
|
||||||
|
"displayName": "Token Exchange",
|
||||||
|
"attributes": {},
|
||||||
|
"uris": [],
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "token-exchange"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"name": "allow-nextcloud-mcp-server-to-exchange",
|
||||||
|
"description": "",
|
||||||
|
"type": "client",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "UNANIMOUS",
|
||||||
|
"config": {
|
||||||
|
"clients": "[\"nextcloud-mcp-server\",\"nextcloud\"]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token-exchange-permission",
|
||||||
|
"description": "",
|
||||||
|
"type": "scope",
|
||||||
|
"logic": "POSITIVE",
|
||||||
|
"decisionStrategy": "AFFIRMATIVE",
|
||||||
|
"config": {
|
||||||
|
"resources": "[\"token-exchange\"]",
|
||||||
|
"scopes": "[\"token-exchange\"]",
|
||||||
|
"applyPolicies": "[\"allow-nextcloud-mcp-server-to-exchange\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "token-exchange",
|
||||||
|
"displayName": "Token Exchange"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decisionStrategy": "UNANIMOUS"
|
||||||
|
},
|
||||||
"fullScopeAllowed": true,
|
"fullScopeAllowed": true,
|
||||||
"nodeReRegistrationTimeout": -1
|
"nodeReRegistrationTimeout": -1
|
||||||
},
|
},
|
||||||
@@ -229,6 +280,18 @@
|
|||||||
"fullScopeAllowed": true,
|
"fullScopeAllowed": true,
|
||||||
"nodeReRegistrationTimeout": -1,
|
"nodeReRegistrationTimeout": -1,
|
||||||
"protocolMappers": [
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "mcp-server-audience",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-audience-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"included.client.audience": "nextcloud-mcp-server",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"id.token.claim": "false",
|
||||||
|
"introspection.token.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextcloud-audience",
|
"name": "nextcloud-audience",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
|
|||||||
@@ -140,27 +140,23 @@ class ProgressiveConsentTokenVerifier:
|
|||||||
|
|
||||||
# Audience validation:
|
# Audience validation:
|
||||||
# - Accept tokens with no audience (will validate via introspection if needed)
|
# - Accept tokens with no audience (will validate via introspection if needed)
|
||||||
# - Accept tokens with MCP client ID in audience
|
# - Accept tokens with MCP client ID in audience (regardless of other audiences)
|
||||||
# - Reject tokens with "nextcloud" audience (wrong flow)
|
# - Reject tokens without MCP client ID (if audience is present)
|
||||||
if audiences:
|
if audiences:
|
||||||
# Check if this is a Nextcloud token (wrong flow)
|
# Check if MCP client ID is in the audience
|
||||||
if "nextcloud" in audiences:
|
if self.mcp_client_id in audiences:
|
||||||
|
logger.debug(
|
||||||
|
f"Token has audience {audiences} - MCP client ID present"
|
||||||
|
)
|
||||||
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Token rejected: wrong audience {audiences}, expected {self.mcp_client_id} or no audience"
|
f"Token rejected: wrong audience {audiences}, expected {self.mcp_client_id} or no audience"
|
||||||
)
|
)
|
||||||
logger.error(
|
logger.error(
|
||||||
"Received Nextcloud token in MCP context - "
|
"Token does not include MCP client ID in audience - "
|
||||||
"client may be using wrong token"
|
"client may be using wrong token"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# If audience is present but doesn't match, log warning but continue
|
|
||||||
# (token might use resource URL instead of client ID)
|
|
||||||
if self.mcp_client_id not in audiences:
|
|
||||||
logger.info(
|
|
||||||
f"Token has audience {audiences}, expected {self.mcp_client_id}. "
|
|
||||||
"Accepting token with non-standard audience (may use resource URL)."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Token has no audience claim - accepting for MCP server validation"
|
"Token has no audience claim - accepting for MCP server validation"
|
||||||
|
|||||||
Reference in New Issue
Block a user