diff --git a/keycloak/realm-export.json b/keycloak/realm-export.json index 88a6928..27cd8b1 100644 --- a/keycloak/realm-export.json +++ b/keycloak/realm-export.json @@ -177,7 +177,8 @@ "standardFlowEnabled": false, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, "publicClient": false, "protocol": "openid-connect", "attributes": { @@ -186,6 +187,56 @@ "client.token.exchange.standard.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, "nodeReRegistrationTimeout": -1 }, @@ -229,6 +280,18 @@ "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "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", "protocol": "openid-connect", diff --git a/nextcloud_mcp_server/auth/progressive_token_verifier.py b/nextcloud_mcp_server/auth/progressive_token_verifier.py index 20db676..ff83d5d 100644 --- a/nextcloud_mcp_server/auth/progressive_token_verifier.py +++ b/nextcloud_mcp_server/auth/progressive_token_verifier.py @@ -140,27 +140,23 @@ class ProgressiveConsentTokenVerifier: # Audience validation: # - Accept tokens with no audience (will validate via introspection if needed) - # - Accept tokens with MCP client ID in audience - # - Reject tokens with "nextcloud" audience (wrong flow) + # - Accept tokens with MCP client ID in audience (regardless of other audiences) + # - Reject tokens without MCP client ID (if audience is present) if audiences: - # Check if this is a Nextcloud token (wrong flow) - if "nextcloud" in audiences: + # Check if MCP client ID is in the audience + if self.mcp_client_id in audiences: + logger.debug( + f"Token has audience {audiences} - MCP client ID present" + ) + else: logger.warning( f"Token rejected: wrong audience {audiences}, expected {self.mcp_client_id} or no audience" ) logger.error( - "Received Nextcloud token in MCP context - " + "Token does not include MCP client ID in audience - " "client may be using wrong token" ) 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: logger.info( "Token has no audience claim - accepting for MCP server validation"