Skip to content

fix(keycardai-oauth): impersonate() accepts a client assertion (ECO-455) - #366

Merged
Larry-Osakwe merged 1 commit into
mainfrom
devin/1792518984-impersonate-assertion
Oct 20, 2026
Merged

Larry-Osakwe merged 1 commit into
mainfrom
devin/1792518984-impersonate-assertion

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

AsyncClient.impersonate() and Client.impersonate() had no way to carry a client assertion, although exchange_token() and client_credentials_grant() in the same client accept client_assertion / client_assertion_type. An application whose credential is a workload identity (no client secret) therefore could not authenticate the substitute-user exchange. Parity half of ECO-455; the TypeScript fix is typescript-sdk #265.

Both methods take two new keyword-only parameters and pass them into the TokenExchangeRequest exactly as exchange_token() does:

async def impersonate(self, *, user_identifier, resource, scope=None, timeout=None, issuer=None,
                      client_assertion: str | None = None,
                      client_assertion_type: str | None = None) -> TokenResponse:
    request = TokenExchangeRequest(
        subject_token=build_substitute_user_token(user_identifier),
        subject_token_type=TokenType.SUBSTITUTE_USER,
        resource=resource,
        scope=scope,
        client_assertion=client_assertion,
        client_assertion_type=client_assertion_type,
    )

The request serializer drops None fields (model_dump(exclude_none=True)), so omitting them produces the same bytes as before. No validation was added: exchange_token() does not require client_assertion_type when an assertion is given, and impersonate() follows it rather than inventing a stricter rule in one method. Docstring examples on both methods show the workload identity call.

Tests

tests/keycardai/oauth/test_impersonate_client_assertion.py, sync and async:

  • the assertion fields reach the form body, subject_token_type stays the substitute-user URN, no Authorization header is sent (both fail against main with TypeError: unexpected keyword argument 'client_assertion')
  • omitting them is byte-for-byte unchanged: the body equals the one exchange_token() produces for the same substitute-user TokenExchangeRequest, and contains no client_assertion

Verification

packages/oauth: pytest  565 passed
ruff check packages/oauth: All checks passed!
just typecheck (ty, all packages): All checks passed!

No manual version or CHANGELOG edits; fix(keycardai-oauth) cuts the release.

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

Both impersonate() methods take keyword-only client_assertion and client_assertion_type and pass them into the TokenExchangeRequest the way exchange_token() does, so an application whose credential is a workload identity can authenticate the substitute-user exchange in the request body. Omitting them leaves the wire request unchanged.

Co-Authored-By: Larry Osakwe <larry@keycard.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • 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.

Reviewed against ECO-455: the assertion reaches the form body, Basic auth is unchanged for client secrets, tests pin both.

@Larry-Osakwe
Larry-Osakwe merged commit 7b02c95 into main Oct 20, 2026
14 checks passed
@Larry-Osakwe
Larry-Osakwe deleted the devin/1792518984-impersonate-assertion branch October 20, 2026 20:55
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