feat: support private_key_jwt client authentication#718
Open
andrey-berenda wants to merge 4 commits into
Open
Conversation
- Allow client_id=None on KeycloakOpenID (sanctions the runtime behavior required when the auth server derives client identity from the assertion's `sub` claim). - Add `client_assertion` constructor parameter (str or zero-arg callable). When set, _add_secret_key emits client_assertion_type/client_assertion and skips client_secret. Closes marcospereirampj#717. - Add per-call `client_assertion` parameter on token(). - Tests for instance/callable/per-call assertions, suppression of client_secret, and the unchanged secret path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plumbs `client_assertion` through KeycloakOpenIDConnection into the underlying KeycloakOpenID, so KeycloakAdmin works natively with private_key_jwt clients (including auto-refresh, which re-invokes the configured assertion callable). - `client_id: str` -> `client_id: str | None` on KeycloakOpenIDConnection. - New `client_assertion: str | Callable[[], str] | None` parameter, forwarded to the inner KeycloakOpenID constructor. - `grant_type` defaults to `client_credentials` when an assertion is set. - `keycloak_openid` accessor now requires either `client_id` or `client_assertion` (not just `client_id`). - Tests for assertion plumbing, callable laziness, and the missing-creds error path. Verified end-to-end against a live Keycloak realm with a Kubernetes SA JWT: get_realm/get_users/get_clients all succeed; callable assertion is invoked once per token mint and re-invoked on forced re-issue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves CI lint failures introduced by the previous two commits: - Move `Callable` import to `collections.abc` and into a TYPE_CHECKING block (UP035 + TC003); both modules already use `from __future__ import annotations`, so it is type-only. - Suppress S106 on three test fixtures that intentionally pass dummy values to `client_secret_key` to verify wire-payload behavior. - Apply `ruff format` to the new test block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`test_counter_part` enforces parameter parity between sync `token()` and async `a_token()`. The previous commit added `client_assertion` only to the sync method, which broke that invariant on Python 3.10/22.0 CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Heads-up on the failing checks: the failures are limited to the The same failures are present on All pinned Keycloak versions (22.0–26.0) on every Python version are green. Happy to rebase once |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First-class
private_key_jwt(RFC 7521 / RFC 7523) support in bothKeycloakOpenIDandKeycloakOpenIDConnection/KeycloakAdmin.KeycloakOpenIDclient_id: str→client_id: str | None. Sanctions the runtime behavior required when the auth server derives client identity from the assertion'ssubclaim (requestsalready stripsNone-valued keys fromdata=).client_assertionconstructor parameter —stror zero-argCallable[[], str]. When set, the token endpoint receivesclient_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer+client_assertion=<jwt>, andclient_secretis not sent.client_assertion=argument ontoken()for callers that mint a fresh JWT per request.KeycloakOpenIDConnection(and thereforeKeycloakAdmin)client_assertionplumbed through into the underlyingKeycloakOpenID, so the admin client works natively withprivate_key_jwt— including auto-refresh, which re-invokes a callable assertion on each mint.grant_typedefaults toclient_credentialswhen only an assertion is set.client_idmay now beNoneas long asclient_assertionis set.Closes #717.
Behavior matrix
client_secret_keyonly (status quo)client_secret=...client_assertion(str)client_assertion_type=...&client_assertion=<jwt>client_assertion(callable)client_secret_keyandclient_assertionsetclient_secrettoken(client_assertion=...)client_assertion_typeauto-addedUsage
Test plan
tests/test_keycloak_openid.py(assertion str/callable, suppression ofclient_secret, secret-only path unchanged,token()payload shape).tests/test_keycloak_admin.py(assertion plumbing, callable laziness, missing-creds error).private_key_jwt(Kubernetes ServiceAccount JWT, EKS OIDC issuer):KeycloakOpenIDwith str / callable / per-call /client_secret_key+client_assertionco-existence — all200 OK.KeycloakAdminviaKeycloakOpenIDConnection(client_assertion=...)—get_realm,get_users(384),get_clients(7) all succeed; callable invoked once per mint and re-invoked on forced re-issue.🤖 Generated with Claude Code