Skip to content

feat(keycardai-starlette): expose the caller identity claims on the auth context - #269

Merged
Larry-Osakwe merged 2 commits into
mainfrom
devin/1788565573-identity-claims-auth-context
Sep 5, 2026
Merged

feat(keycardai-starlette): expose the caller identity claims on the auth context#269
Larry-Osakwe merged 2 commits into
mainfrom
devin/1788565573-identity-claims-auth-context

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Implements the auth-context identity fields from specs/server-bearer-auth/bearer-token-verification-middleware.md (unit row 4) for Python, following the Ruby reference in keycardai/ruby-sdk#24 (ECO-367).

KeycardUser in keycardai-starlette gains sub, sub_profile and keycard_app_id alongside the existing client_id, all read from the verified token. The class docstring says which one to key on (keycard_app_id for the application, client_id for the credential, sub for the subject, sub_profile to tell a user token from an application token) so a consumer does not infer it from sub against client_id.

Cross-package change. The claims reach the backend through TokenVerifier.verify_token, whose return type is AccessToken in keycardai-oauth (packages/oauth/src/keycardai/oauth/server/verifier.py). That model is the natural carrier, so it gains the same three fields and _verify_token populates them:

class AccessToken(BaseModel):
    token: str
    client_id: str
    scopes: list[str]
    expires_at: int | None = None
    resource: str | None = None
    sub: str | None = None
    sub_profile: str | None = None
    keycard_app_id: str | None = None

sub comes from the RFC 9068 sub field of JWTAccessToken; sub_profile and keycard_app_id come from get_custom_claim. All three are optional (None when absent) and never fail verification. sub is already required by parse_jwt_access_token, so it is present on any conforming token, but the field stays optional per the spec rather than adding a second check.

Tests. oauth: a Keycard token populates all four fields; a token without the Keycard claims verifies with sub_profile and keycard_app_id None. starlette: the same two cases through KeycardAuthBackend.authenticate, plus KeycardUser default and application-token (sub == keycard_app_id, sub_profile == "app") cases. Existing tests are untouched except for two shared fixture helpers: create_mock_jwt_access_token in the oauth verifier tests now sets token.sub (the Mock(spec=JWTAccessToken) otherwise raises on the new attribute read) and _stub_verifier in the starlette tests accepts the identity fields. New claims are read before resource in _verify_token so the existing assert_called_with("resource") assertion holds.

MCP surface. packages/mcp re-exports AccessToken from keycardai.oauth.server.verifier, so its verifier surface picks up the fields automatically. Its @grant path reads request.user (the starlette KeycardUser) into a dict of access_token, zone_id and resource_server_url for token exchange rather than wrapping or re-exporting KeycardUser, so nothing there needed extending. packages/a2a propagates the KeycardUser object itself and inherits the fields.

Gates: oauth, starlette and a2a suites green; ruff clean; ty diagnostics unchanged from main; changelog.py validate passes. Two mcp integration tests (test_auth_provider_init_with_zone_id, test_no_application_credential_does_enable_registration) fail identically on main in this environment and are unrelated.

The keycard-sdk-spec divergence row is left as is until all three languages land.

Link to Devin session: https://app.devin.ai/sessions/bbc062cf32b548ef9c4b8b444d2ec67f
Open in Devin Desktop: https://app.devin.ai/desktop/session/bbc062cf32b548ef9c4b8b444d2ec67f?variant=devin
Requested by: @Larry-Osakwe

…uth context

KeycardUser gains sub, sub_profile and keycard_app_id, populated from the verified token. The verifier's AccessToken in keycardai-oauth carries the claims from the JWT to the backend, so it gains the same three optional fields.

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

KeycardUser now reads sub, sub_profile and keycard_app_id off the
verifier's AccessToken, fields that first exist in the oauth release
cut from this PR. The workspace resolves locally so tests cannot see
the gap; a consumer on any published oauth would AttributeError in
authenticate.
@Larry-Osakwe
Larry-Osakwe merged commit b987536 into main Sep 5, 2026
8 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1788565573-identity-claims-auth-context branch September 5, 2026 00:18
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