Skip to content

feat(keycardai-oauth): OIDC discovery fields and UserInfo capability - #232

Merged
Larry-Osakwe merged 2 commits into
mainfrom
devin/1787592627-oauth-userinfo
Aug 24, 2026
Merged

feat(keycardai-oauth): OIDC discovery fields and UserInfo capability#232
Larry-Osakwe merged 2 commits into
mainfrom
devin/1787592627-oauth-userinfo

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

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 in metadata.raw.

Discovery (AuthorizationServerMetadata) now types both fields, and absence stays non-fatal:

userinfo_endpoint: str | None = None      # OIDC Discovery 1.0 §3
end_session_endpoint: str | None = None   # OIDC RP-Initiated Logout 1.0 §2.1

New operations/_userinfo.py plus Client.userinfo() / AsyncClient.userinfo():

with Client("https://zone.keycard.cloud") as client:
    user = client.userinfo(access_token)   # user.sub, user.claims (unfiltered)

Behavior worth calling out, matching the spec's contract table:

  • Endpoint comes from metadata only. The client reuses metadata it already discovered (new self._discovered_metadata, set during _ensure_initialized), or accepts pre-discovered metadata via metadata=; no userinfo entry was added to Endpoints, so a server that doesn't advertise userinfo_endpoint is a ConfigError raised before any HTTP request.
  • The request is a single GET with Accept: application/json and Authorization: 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.
  • UserInfoResponse exposes the validated sub alongside the complete claims dict; nothing is filtered to a known set, so custom/provider claims pass through.
  • Error mapping: 401InvalidTokenError (RFC 6750 invalid_token, error code parsed from the WWW-Authenticate challenge when present); other non-2xx → OAuthHttpError; invalid JSON, non-object body, missing/empty sub, and application/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.md documents the operation and the two new metadata fields. docs/sdk/*.mdx is left untouched: regenerating it with just sdk-ref-oauth pulls in ~600 lines of unrelated drift from modules this PR doesn't touch, which belongs in its own refresh.

Verification: ruff check, pytest for oauth (355 tests) plus starlette/mcp/a2a/langchain all pass. just typecheck (ty) isn't runnable here — ty is not declared as a dependency anywhere in the workspace and no CI job runs it; uvx ty check packages/oauth/src reports only pre-existing diagnostic classes, with the new module's context.transport.request_raw union warning identical to the existing _discovery.py/_registration.py ones.

Link to Devin session: https://app.devin.ai/sessions/911c2391f4104fb591d8038487bbba56
Requested by: @Larry-Osakwe

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-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@Larry-Osakwe Larry-Osakwe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/oauth/src/keycardai/oauth/operations/_userinfo.py
Comment thread packages/oauth/src/keycardai/oauth/client.py
Co-Authored-By: Larry Osakwe <larry@keycard.ai>
@Larry-Osakwe
Larry-Osakwe merged commit 877b658 into main Aug 24, 2026
5 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1787592627-oauth-userinfo branch August 24, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants