From 3b20835cd1a80bc104b2159dd4b540aac82e56d1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 19 May 2026 18:31:50 -0700 Subject: [PATCH 1/2] fix(fastmcp): accept zone_url as valid audience in AuthProvider Keycard PKCE access tokens carry aud=zone_url rather than aud=resource_url (the MCP resource is indicated by a separate `resource` claim). The previous implementation set audience to only the MCP resource URL, causing token validation to fail with invalid_token for all Keycard-issued PKCE tokens. Fix: set audience to a list containing both the resource URL and zone_url, so the JWTVerifier accepts either form. --- packages/fastmcp/src/keycardai/fastmcp/provider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/fastmcp/src/keycardai/fastmcp/provider.py b/packages/fastmcp/src/keycardai/fastmcp/provider.py index 8be867c..79c3cf4 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() From 885b382970a89acb0852a4d13404bf591c443834 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 19 May 2026 21:41:53 -0700 Subject: [PATCH 2/2] fix(fastmcp): use stable WIF key_id as resource_client_id in grant() The grant() decorator built auth_info with resource_client_id set to self.client.config.client_id, which for WebIdentity is the DCR-registered ua: identifier. This changes on every restart and cannot be pre-registered as a Keycard application credential. Fix: when application_credential has an identity_manager (WebIdentity), use its stable key_id instead. This matches the identifier registered via keycard agent api application-credentials --type public-key. --- packages/fastmcp/src/keycardai/fastmcp/provider.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/fastmcp/src/keycardai/fastmcp/provider.py b/packages/fastmcp/src/keycardai/fastmcp/provider.py index 79c3cf4..b06146b 100644 --- a/packages/fastmcp/src/keycardai/fastmcp/provider.py +++ b/packages/fastmcp/src/keycardai/fastmcp/provider.py @@ -733,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": "", }