diff --git a/packages/langchain/pyproject.toml b/packages/langchain/pyproject.toml index 6e48edd..cffc30a 100644 --- a/packages/langchain/pyproject.toml +++ b/packages/langchain/pyproject.toml @@ -8,7 +8,7 @@ license = { text = "MIT" } authors = [{ name = "Keycard", email = "support@keycard.ai" }] dependencies = [ "httpx>=0.27.2", - "keycardai-oauth>=0.9.0", + "keycardai-oauth>=0.24.0", "langchain>=1.0", "langgraph>=1.0", ] diff --git a/packages/langchain/src/keycardai/langchain/middleware.py b/packages/langchain/src/keycardai/langchain/middleware.py index ab9c7f5..610ddd1 100644 --- a/packages/langchain/src/keycardai/langchain/middleware.py +++ b/packages/langchain/src/keycardai/langchain/middleware.py @@ -293,9 +293,11 @@ async def _client_auth_fields( client=client, subject_token="client-credentials", resource=resource ) fields: dict[str, str] = {} - if getattr(prepared, "client_assertion", None): + if prepared.client_assertion: fields["client_assertion"] = prepared.client_assertion fields["client_assertion_type"] = prepared.client_assertion_type + if prepared.client_id: + fields["client_id"] = prepared.client_id return fields async def _grant_as_self( diff --git a/packages/langchain/tests/test_middleware.py b/packages/langchain/tests/test_middleware.py index c9575c3..5bcd6f9 100644 --- a/packages/langchain/tests/test_middleware.py +++ b/packages/langchain/tests/test_middleware.py @@ -397,6 +397,9 @@ class StubAssertionCredential: """ApplicationCredential whose proof rides in the request body, the shape WorkloadIdentity and WebIdentity use.""" + def __init__(self, client_id: str | None = None) -> None: + self.client_id = client_id + def get_http_client_auth(self): # noqa: ANN201 from keycardai.oauth import NoneAuth @@ -414,6 +417,7 @@ async def prepare_token_exchange_request( subject_token_type="urn:ietf:params:oauth:token-type:access_token", client_assertion="stub-assertion", client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_id=self.client_id, ) @@ -468,7 +472,7 @@ def test_credential_assertion_reaches_the_as_self_grant() -> None: middleware = KeycardGrantMiddleware( resources=[RESOURCE], client=stub, - application_credential=StubAssertionCredential(), + application_credential=StubAssertionCredential(client_id="agent"), ) with middleware.grant(KeycardIdentity(as_self=True)) as access: assert access.access(RESOURCE).access_token == f"self-token-for-{RESOURCE}" @@ -476,6 +480,19 @@ def test_credential_assertion_reaches_the_as_self_grant() -> None: assert call["resource"] == RESOURCE assert call["client_assertion"] == "stub-assertion" assert call["client_assertion_type"].endswith("jwt-bearer") + assert call["client_id"] == "agent" + + +def test_credential_assertion_without_client_id_omits_it_from_as_self() -> None: + stub = StubExchangeClient() + middleware = KeycardGrantMiddleware( + resources=[RESOURCE], + client=stub, + application_credential=StubAssertionCredential(), + ) + with middleware.grant(KeycardIdentity(as_self=True)) as access: + assert access.access(RESOURCE).access_token == f"self-token-for-{RESOURCE}" + assert "client_id" not in stub.self_calls[0] def test_partial_grant_yields_token_and_resource_error_side_by_side() -> None: