Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/langchain/pyproject.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
]
Expand Down
4 changes: 3 additions & 1 deletion packages/langchain/src/keycardai/langchain/middleware.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(
Expand Down
19 changes: 18 additions & 1 deletion packages/langchain/tests/test_middleware.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand All@@ -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,
)


Expand DownExpand Up@@ -468,14 +472,27 @@ 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}"
call = stub.self_calls[0]
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:
Expand Down
Loading