Skip to content

fix(authz): use standard Keycloak token exchange#3754

Open
jp-ayyappan wants to merge 2 commits into
mainfrom
DSPX-3636-standard-token-exchange
Open

fix(authz): use standard Keycloak token exchange#3754
jp-ayyappan wants to merge 2 commits into
mainfrom
DSPX-3636-standard-token-exchange

Conversation

@jp-ayyappan

@jp-ayyappan jp-ayyappan commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • provision Keycloak standard token exchange by enabling standard.token.exchange.enabled on requester clients
  • stop using the legacy client management-permissions token-exchange setup path
  • bump local/DPoP token-exchange Keycloak paths to 26.2 and remove preview token-exchange feature flags
  • document that token_exchanges entries enable standard token exchange on requester clients

Open Questions

  • service/entityresolution/integration/keycloak_test.go still starts a separate quay.io/keycloak/keycloak:23.0 container using KEYCLOAK_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 ./... from lib/fixtures
  • ruby -e 'require "yaml"; YAML.load_file("docker-compose.yaml"); YAML.load_file("test/start-up-with-containers/action.yaml"); puts "ok"'
  • git diff --check

Related

  • DSPX-3636
  • Supports virtru-corp/data-security-platform#3450
  • Supports virtru-corp/dsp-bundle#275

Summary by CodeRabbit

  • New Features

    • Added support for standard Keycloak token exchange configuration on requester clients.
    • Updated setup guidance for configuring token exchanges.
  • Improvements

    • Upgraded Keycloak provisioning and integration environments to version 26.2.
    • Updated administrative configuration for compatibility with the newer Keycloak release.
  • Tests

    • Added coverage for enabling standard token exchange while preserving existing client attributes.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Keycloak token exchange

Layer / File(s) Summary
Keycloak 26.2 runtime alignment
docker-compose.yaml, test/integration/oauth/oauth_test.go, test/start-up-with-containers/action.yaml
Runtime and OAuth test containers use Keycloak 26.2 and bootstrap admin variables; the DPoP-specific overlay step is removed.
Standard token exchange provisioning
lib/fixtures/keycloak.go, lib/fixtures/keycloak_test.go, service/README.md
Token-exchange setup enables standard exchange on requester clients, with attribute-preservation tests and updated provisioning guidance.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: docs

Poem

I’m a rabbit with carrots in flight,
Keycloak now hops to twenty-six point two bright.
Requesters exchange tokens with ease,
Tests guard their attributes among the trees.
Bootstrap flags bloom in the breeze!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: switching authz to standard Keycloak token exchange.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3636-standard-token-exchange

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Keycloak Token Exchange Modernization: Migrated from legacy client management-permissions to standard Keycloak token exchange by enabling the 'standard.token.exchange.enabled' attribute on requester clients.
  • Keycloak Version Upgrade: Updated local and DPoP-related Keycloak environments to version 26.2 and removed deprecated preview feature flags.
  • Configuration Updates: Updated environment variables to use the modern 'KC_BOOTSTRAP_ADMIN' naming convention for Keycloak administrative credentials.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread lib/fixtures/keycloak.go Outdated
Comment on lines +1087 to +1091
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])

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.

high

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.

Suggested change
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)

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 193.706618ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 109.843497ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 454.706137ms
Throughput 219.92 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 53.620642959s
Average Latency 534.337762ms
Throughput 93.25 requests/second

@jp-ayyappan jp-ayyappan changed the title DSPX-3636 use standard Keycloak token exchange fix(authz): use standard Keycloak token exchange Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • examples
  • otdfctl
  • sdk
  • service
  • lib/fixtures
  • tests-bdd

See the workflow run for details.

@github-actions

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 210.496559ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 115.550387ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 425.887497ms
Throughput 234.80 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 49.272363121s
Average Latency 490.730245ms
Throughput 101.48 requests/second

// 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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
userTokenClientID = "opentdf-sdk"
userTokenClientID = "opentdf-sdk". //nolint:gosec // Test Credential

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants