Uh oh!
There was an error while loading. Please reload this page.
feat(keycardai-oauth): OIDC discovery fields and UserInfo capability - #232
Conversation
Types userinfo_endpoint and end_session_endpoint on AuthorizationServerMetadata and adds Client.userinfo()/AsyncClient.userinfo() per OIDC Core 1.0 Section 5.3. Implements keycardai/keycard-sdk-spec#45. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Larry-Osakwe
left a comment
There was a problem hiding this comment.
Two small asks, nothing structural. I checked the load-bearing parts: auth headers in this codebase are applied per operation, never by the transport, so the bearer-instead-of-client-auth design is sound and the assert_not_called test pins it. The discovery parser wiring for the new fields is there with both present and absent cases, and the spec's unit table is fully covered, including application/jwt rejection and no-request-on-missing-endpoint.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-Authored-By: Larry Osakwe <larry@keycard.ai>
Summary
Implements keycard-sdk-spec#45 (
spec(oauth-client): OIDC discovery fields and UserInfo capability) for the Python SDK, closing the tracked divergence where these discovery fields survived only inmetadata.raw.Discovery (
AuthorizationServerMetadata) now types both fields, and absence stays non-fatal:New
operations/_userinfo.pyplusClient.userinfo()/AsyncClient.userinfo():Behavior worth calling out, matching the spec's contract table:
self._discovered_metadata, set during_ensure_initialized), or accepts pre-discovered metadata viametadata=; nouserinfoentry was added toEndpoints, so a server that doesn't advertiseuserinfo_endpointis aConfigErrorraised before any HTTP request.GETwithAccept: application/jsonandAuthorization: Bearer <access_token>. The client's own auth strategy is deliberately not applied — UserInfo authenticates the user's token, not the client — so the bearer header is written last, after context headers.UserInfoResponseexposes the validatedsubalongside the complete claims dict; nothing is filtered to a known set, so custom/provider claims pass through.401→InvalidTokenError(RFC 6750invalid_token, error code parsed from theWWW-Authenticatechallenge when present); other non-2xx →OAuthHttpError; invalid JSON, non-object body, missing/emptysub, andapplication/jwt(signed responses unsupported) →OAuthProtocolError. No caching and no POST form, per spec.Tests cover the spec's unit-test table (
tests/.../operations/test_userinfo.py), the two discovery cases (fields present / absent), and client-level wiring for both sync and async, including that no request is made when the endpoint is missing.packages/oauth/README.mddocuments the operation and the two new metadata fields.docs/sdk/*.mdxis left untouched: regenerating it withjust sdk-ref-oauthpulls in ~600 lines of unrelated drift from modules this PR doesn't touch, which belongs in its own refresh.Verification:
ruff check,pytestfor oauth (355 tests) plus starlette/mcp/a2a/langchain all pass.just typecheck(ty) isn't runnable here —tyis not declared as a dependency anywhere in the workspace and no CI job runs it;uvx ty check packages/oauth/srcreports only pre-existing diagnostic classes, with the new module'scontext.transport.request_rawunion warning identical to the existing_discovery.py/_registration.pyones.Link to Devin session: https://app.devin.ai/sessions/911c2391f4104fb591d8038487bbba56
Requested by: @Larry-Osakwe