fix(authz): use standard Keycloak token exchange#3754
Conversation
📝 WalkthroughWalkthroughKeycloak is upgraded to 26.2 with updated bootstrap variables. Token-exchange fixture setup now enables standard exchange on requester clients, with tests and provisioning documentation updated accordingly. ChangesKeycloak token exchange
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request modernizes the Keycloak token exchange implementation by moving away from complex, legacy management-permission setups in favor of the standard Keycloak token exchange mechanism. Additionally, it upgrades the development and testing Keycloak images to version 26.2, cleans up environment configuration, and updates documentation to reflect the new provisioning approach. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. The legacy path we leave behind, A standard way we now shall find. With Keycloak bumped to twenty-six, We've fixed the tokens in the mix. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request upgrades Keycloak from version 25.0 to 26.2 across docker-compose, integration tests, and CI actions. It also simplifies the token exchange configuration by enabling the standard token exchange attribute directly on the requester client instead of creating complex fine-grained admin permissions and policies. A critical feedback point was raised regarding the retrieval of the requester client: Keycloak's GetClients API can return multiple clients if the ID is a prefix of another, so relying on the first element of the returned slice could lead to non-deterministic behavior. A code suggestion is provided to iterate and find the exact match.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if len(requesterClients) == 0 { | ||
| return fmt.Errorf("token exchange requester client %q not found", startClientID) | ||
| } | ||
| tokenExchangePolicyPermissionResourceID := mgmtPermissionsRepr.Resource | ||
| scopePermissions := *mgmtPermissionsRepr.ScopePermissions | ||
| tokenExchangePolicyScopePermissionID := scopePermissions["token-exchange"] | ||
| slog.Debug("creating management permission", | ||
| slog.String("resource", *tokenExchangePolicyPermissionResourceID), | ||
| slog.String("scope_permission_id", tokenExchangePolicyScopePermissionID)) | ||
|
|
||
| slog.Debug("step 2 - get realm mgmt client id") | ||
| realmMangementClientName := "realm-management" | ||
| realmManagementClientID, err := getIDOfClient(ctx, tm, connectParams, &realmMangementClientName) | ||
| if err != nil { | ||
| return err | ||
| requesterClient := withStandardTokenExchangeEnabled(*requesterClients[0]) |
There was a problem hiding this comment.
Keycloak's GetClients API performs a search/filter on the clientId parameter, which can return multiple clients if one client ID is a prefix of or contains another (e.g., searching for "opentdf" might return both "opentdf" and "opentdf-sdk"). Relying on requesterClients[0] can lead to non-deterministic behavior or enabling token exchange on the wrong client. We should iterate through the results to find the exact match.
| if len(requesterClients) == 0 { | |
| return fmt.Errorf("token exchange requester client %q not found", startClientID) | |
| } | |
| tokenExchangePolicyPermissionResourceID := mgmtPermissionsRepr.Resource | |
| scopePermissions := *mgmtPermissionsRepr.ScopePermissions | |
| tokenExchangePolicyScopePermissionID := scopePermissions["token-exchange"] | |
| slog.Debug("creating management permission", | |
| slog.String("resource", *tokenExchangePolicyPermissionResourceID), | |
| slog.String("scope_permission_id", tokenExchangePolicyScopePermissionID)) | |
| slog.Debug("step 2 - get realm mgmt client id") | |
| realmMangementClientName := "realm-management" | |
| realmManagementClientID, err := getIDOfClient(ctx, tm, connectParams, &realmMangementClientName) | |
| if err != nil { | |
| return err | |
| requesterClient := withStandardTokenExchangeEnabled(*requesterClients[0]) | |
| var matchedClient *gocloak.Client | |
| for _, c := range requesterClients { | |
| if c.ClientID != nil && *c.ClientID == startClientID { | |
| matchedClient = c | |
| break | |
| } | |
| } | |
| if matchedClient == nil { | |
| return fmt.Errorf("token exchange requester client %q not found", startClientID) | |
| } | |
| requesterClient := withStandardTokenExchangeEnabled(*matchedClient) |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
| // Standard OAuth grant-type and token-type URNs from RFC 8693 — not credentials. | ||
| tokenExchangeGrant = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // URN identifier, not a credential | ||
| accessTokenType = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec // URN identifier, not a credential | ||
| userTokenClientID = "opentdf-sdk" |
There was a problem hiding this comment.
| userTokenClientID = "opentdf-sdk" | |
| userTokenClientID = "opentdf-sdk". //nolint:gosec // Test Credential |
Summary
standard.token.exchange.enabledon requester clientstoken_exchangesentries enable standard token exchange on requester clientsOpen Questions
service/entityresolution/integration/keycloak_test.gostill starts a separatequay.io/keycloak/keycloak:23.0container usingKEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD. That test does not exercise token exchange or provisioning, so this PR leaves it pinned. Should that ERS-only Keycloak fixture remain intentionally pinned to KC23, or should it be tracked as a separate Keycloak modernization follow-up?Testing
go test -short ./...fromlib/fixturesruby -e 'require "yaml"; YAML.load_file("docker-compose.yaml"); YAML.load_file("test/start-up-with-containers/action.yaml"); puts "ok"'git diff --checkRelated
Summary by CodeRabbit
New Features
Improvements
Tests