diff --git a/packages/fastmcp/src/keycardai/fastmcp/provider.py b/packages/fastmcp/src/keycardai/fastmcp/provider.py index 8be867c..b06146b 100644 --- a/packages/fastmcp/src/keycardai/fastmcp/provider.py +++ b/packages/fastmcp/src/keycardai/fastmcp/provider.py @@ -416,7 +416,9 @@ def __init__( self.mcp_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" # fastmcp automatically appends `/mcp` to the base_url when presenting Protected Resource to the clients. # we need to append `/mcp` to the mcp_base_url to ensure the audience is properly aligned with FastMCP JWTVerifier. - self.audience = f"{self.mcp_base_url}mcp" + # Also accept zone_url as a valid audience: Keycard PKCE access tokens carry aud=zone_url + # (the resource is in a separate `resource` claim), so we must accept both forms. + self.audience = [f"{self.mcp_base_url}mcp", self.zone_url] self.client_name = self.mcp_server_name or "Keycard Auth Client" self.client_factory = client_factory or DefaultClientFactory() @@ -731,9 +733,18 @@ async def wrapper(*args, **kwargs) -> Any: if self.application_credential: logger.debug(f"Using application credential: {type(self.application_credential).__name__}") # auth_info context is used by application credential implementation - # to prepare correct assertions in the token exchange request + # to prepare correct assertions in the token exchange request. + # For WebIdentity, use the stable WIF key_id so the client assertion + # JWT has a predictable `iss` that can be pre-registered in Keycard. + # Falling back to the DCR client_id would produce an ephemeral `ua:...` + # identifier that changes on every restart and cannot be pre-registered. + _resource_client_id = ( + self.application_credential.identity_manager.key_id + if hasattr(self.application_credential, "identity_manager") + else self.client.config.client_id or "" + ) _auth_info = { - "resource_client_id": self.client.config.client_id or "", + "resource_client_id": _resource_client_id, "resource_server_url": self.mcp_base_url, "zone_id": "", }