Skip to content

Feat: IdP-agnostic token exchange plugin interface#521

Merged
huang195 merged 2 commits into
rossoctl:mainfrom
akram:feat/idp-agnostic-token-exchange
Jun 18, 2026
Merged

Feat: IdP-agnostic token exchange plugin interface#521
huang195 merged 2 commits into
rossoctl:mainfrom
akram:feat/idp-agnostic-token-exchange

Conversation

@akram

@akram akram commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces an IdPProvider interface and provider registry so new IdP backends (Entra ID, Okta, etc.) can be added without modifying core plugin code. This is Phase 1 of the IdP-agnostic token exchange work (kagenti-extensions#481 / RHAIENG-5681).

Problem

The token-exchange plugin hardcodes Keycloak-specific assumptions:

  • Token endpoint derivation: /realms/{realm}/protocol/openid-connect/token
  • Config field names: keycloak_url, keycloak_realm
  • JWT assertion type: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
  • Capabilities description: "against Keycloak"

This prevents direct support for Entra ID, Okta, or other RFC 8693-compliant IdPs.

Changes

IdPProvider interface (provider.go)

type IdPProvider interface {
    Name() string
    TokenEndpoint(providerURL, providerRealm string) string
    JWKSEndpoint(providerURL, providerRealm string) string
}

Providers self-register via init() — adding a new IdP requires only:

  1. Create provider_<name>.go
  2. Implement IdPProvider
  3. Call RegisterProvider() in init()
  4. No changes to core plugin code

Keycloak provider (provider_keycloak.go)

Reference implementation as a standalone file, same pattern any contributor follows.

Config generalization (plugin.go)

  • New fields: provider, provider_url, provider_realm for IdP selection
  • Configurable assertion_type on spiffe identity (jwt-spiffe default, jwt-bearer for Okta)
  • Backward compat: keycloak_url/keycloak_realm auto-migrate with deprecation warning
  • applyDefaults() uses LookupProvider() instead of hardcoded switch
  • Capabilities description updated to reflect multi-IdP support

Registration guard test (provider_test.go)

CI test scans provider_*.go files and verifies each has RegisterProvider() in init() — catches missing registration at build time.

Contract documentation (docs/idp-plugin-contract.md)

Full specification for IdP plugin contributors: extension points, config patterns, RFC 8693 compliance analysis, per-IdP auth method matrix, implementation phases.

What does NOT change

  • RFC 8693 token exchange mechanics (grant_type, subject_token, etc.)
  • Route resolver, token cache, plugin registry
  • SPIFFE provider injection, credential file handling
  • Error handling (RFC 6749 OAuth error parsing)

For Entra ID / Okta contributors

To add a new provider, create one file:

// provider_okta.go
package tokenexchange

import "strings"

type oktaProvider struct{}
func (oktaProvider) Name() string { return "okta" }
func (oktaProvider) TokenEndpoint(url, realm string) string { /* ... */ }
func (oktaProvider) JWKSEndpoint(url, realm string) string { /* ... */ }
func init() { RegisterProvider(oktaProvider{}) }

Test plan

  • 6 Keycloak provider tests (token endpoint, JWKS, trailing slash, missing fields)
  • 2 registry tests (unknown returns nil, empty returns nil)
  • 1 registration guard test (scans provider_*.go files)
  • 2 backward compat tests (keycloak_url migration, provider_url preferred)
  • 2 assertion type tests (default jwt-spiffe, configurable jwt-bearer)
  • Full test suite passes (go test ./...) — zero regression
  • Deployed and tested on ROSA HCP cluster with Keycloak

Context

  • Jira: RHAIENG-5681
  • Upstream issue: kagenti-extensions#481
  • Related operator PR: kagenti-operator#436 (egressEnforcement opt-out)

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Token exchange now supports multiple IdPs via provider-driven configuration, including Keycloak and a generic provider model.
    • Added configurable assertion_type to control client authentication.
    • New provider fields: provider, provider_url, provider_realm (with keycloak_url/keycloak_realm kept as backward-compatible aliases).
  • Documentation
    • Published an IdP-agnostic contract for the token-exchange plugin.
  • Tests
    • Expanded coverage for provider resolution, token endpoint derivation, client-auth building, and configuration validation.

@akram
akram requested a review from a team as a code owner June 17, 2026 17:23
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@akram, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09748606-fe22-41ee-ad30-10e4bf10a72f

📥 Commits

Reviewing files that changed from the base of the PR and between 05c0f7a and f904c2f.

📒 Files selected for processing (5)
  • authbridge/authlib/plugins/tokenexchange/plugin.go
  • authbridge/authlib/plugins/tokenexchange/plugin_test.go
  • authbridge/authlib/plugins/tokenexchange/provider.go
  • authbridge/authlib/plugins/tokenexchange/provider_keycloak.go
  • authbridge/docs/idp-plugin-contract.md
📝 Walkthrough

Walkthrough

The token-exchange plugin is generalized from Keycloak-only behavior to a provider-driven, multi-IdP model. A new IdPProvider interface and concurrency-safe global registry are introduced, Keycloak is implemented as the first provider, and the plugin config schema adds provider, provider_url, provider_realm, and configurable assertion_type for SPIFFE identities. Plugin defaults, validation, and runtime client-auth construction are updated to route through registered providers. A formal contract document specifies the extension points and future IdP support.

Changes

Multi-IdP Token-Exchange Plugin

Layer / File(s) Summary
IdPProvider interface and global registry
authbridge/authlib/plugins/tokenexchange/provider.go
Defines the IdPProvider interface with Name, TokenEndpoint, DefaultAssertionType, SupportedIdentityTypes, and BuildClientAuth; introduces IdentityConfig struct; implements a mutex-protected global registry with RegisterProvider (panics on duplicate) and LookupProvider; adds AssertionTypeURN mapping and identity/policy/provider constants.
Keycloak provider implementation
authbridge/authlib/plugins/tokenexchange/provider_keycloak.go
Implements keycloakProvider with all interface methods: Name, TokenEndpoint (trimming trailing slashes, returning empty for missing inputs), DefaultAssertionType, SupportedIdentityTypes (client-secret and spiffe), and BuildClientAuth (validating SPIFFE JWT source, mapping assertion_type through AssertionTypeURN, defaulting to jwt-spiffe). Self-registers via init().
Plugin config schema generalization
authbridge/authlib/plugins/tokenexchange/plugin.go
Updates tokenExchangeConfig and tokenExchangeIdentity schemas to add provider, provider_url, provider_realm, and assertion_type; retains deprecated keycloak_url/keycloak_realm with backward-compat documentation; removes strings import; updates Capabilities() to describe generic RFC 8693 multi-IdP support.
Plugin configuration defaults and validation
authbridge/authlib/plugins/tokenexchange/plugin.go
Reworks applyDefaults() to migrate deprecated keycloak fields, infer provider from legacy fields, derive token_url via LookupProvider, and default identity.assertion_type from provider for SPIFFE. Extends validate() to reject unknown providers, require token_url unless provider inputs exist, validate assertion_type against URN mappings, and check provider↔identity compatibility.
Plugin runtime: client-auth and credential handling
authbridge/authlib/plugins/tokenexchange/plugin.go
Updates Configure() to route client-auth construction to registered provider via buildClientAuth(provider, identity, jwtSrc). Implements new buildClientAuth() fallback that maps assertion_type through AssertionTypeURN, defaults to jwt-spiffe when unset. Updates credentialsAreReady(), pollCredentials(), and utility functions to use exported constants and provider-aware client-auth rebuilding.
Comprehensive test suite
authbridge/authlib/plugins/tokenexchange/plugin_test.go, authbridge/authlib/plugins/tokenexchange/provider_test.go
Adds provider registry validation (TestAllProviderFilesAreRegistered scanning provider_*.go files); Keycloak provider tests (endpoint construction, trailing-slash normalization, default assertion type, supported identity types, BuildClientAuth branching, missing-field returns); LookupProvider nil-return tests; Configure backward-compat and preferred-path tests; buildClientAuth assertion-type defaulting and override tests; validation tests rejecting invalid assertion_type, provider↔identity incompatibility, and unknown providers.
IdP-agnostic plugin contract docs
authbridge/docs/idp-plugin-contract.md
Specifies token_url/jwks_url resolution chain with provider-based derivation, documents ClientAuth extension points with proposed future identity types, backward-compat notes for deprecated fields, explicit non-changes to pipeline behavior, phased implementation plan (Phase 1: this PR, Phase 2: Entra ID, Phase 3: Okta), and per-IdP testing strategy.

Sequence Diagram(s)

sequenceDiagram
  participant Configure
  participant applyDefaults
  participant LookupProvider
  participant keycloakProvider
  participant buildClientAuth

  Configure->>applyDefaults: tokenExchangeConfig
  applyDefaults->>applyDefaults: migrate keycloak_url/keycloak_realm → provider_url/provider_realm
  applyDefaults->>LookupProvider: LookupProvider("keycloak")
  LookupProvider->>keycloakProvider: (lookup in registry)
  keycloakProvider->>keycloakProvider: TokenEndpoint(providerURL, providerRealm)
  keycloakProvider-->>LookupProvider: token_url
  LookupProvider-->>applyDefaults: IdPProvider instance
  applyDefaults->>applyDefaults: default identity.assertion_type from provider.DefaultAssertionType()
  applyDefaults-->>Configure: resolved config with token_url, provider, assertion_type
  Configure->>buildClientAuth: provider, identity (with assertion_type), jwtSrc
  buildClientAuth->>LookupProvider: LookupProvider(provider)
  alt Provider registered
    buildClientAuth->>keycloakProvider: BuildClientAuth(identity, jwtSrc)
    keycloakProvider->>keycloakProvider: map assertion_type → AssertionTypeURN
    keycloakProvider-->>buildClientAuth: ClientAuth
  else Fallback
    buildClientAuth->>buildClientAuth: map assertion_type → AssertionTypeURN, default jwt-spiffe
    buildClientAuth-->>buildClientAuth: ClientAuth
  end
  buildClientAuth-->>Configure: ClientAuth ready
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related issues

Poem

🐇 A rabbit's registry, with mutex and grace,
Holds providers of identity—each in their place.
Keycloak leads the dance; Okta waits to debut,
The assertion type flows where the SPIFFE are true,
And deprecated keycloak_url fades away. 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing an IdP-agnostic token exchange plugin interface.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/authlib/plugins/tokenexchange/plugin.go`:
- Around line 143-152: The backward-compatibility migration logic handles the
deprecated keycloak_url field by defaulting the Provider to "keycloak" when it's
empty, but the keycloak_realm migration block does not perform the same check.
To fix the mixed legacy/new config path issue, add a check in the KeycloakRealm
block that sets c.Provider to "keycloak" if it is empty, similar to the logic
already present in the KeycloakURL block. This ensures that the Provider field
is always populated when any deprecated Keycloak field is present, allowing
proper token URL derivation and config validation.
- Around line 471-474: The code currently silently coerces any unknown
assertionType value to the default "jwt-spiffe" by checking only if urn is
empty. Instead, distinguish between empty input (which should default to
"jwt-spiffe") and invalid non-empty input (which should return an error). Modify
the conditional logic that sets urn to assertionTypeURN["jwt-spiffe"] to only
apply when assertionType is empty or when it doesn't exist in the
assertionTypeURN map, otherwise return an error indicating the unsupported
assertion_type value to prevent config errors from being silently hidden.

In `@authbridge/docs/idp-plugin-contract.md`:
- Around line 209-212: The assertion type support table incorrectly indicates
that Keycloak supports the jwt-bearer assertion type. Update the table row for
the jwt-bearer assertion type to change Keycloak's support status from a
checkmark to an X mark, as Keycloak only supports the jwt-spiffe assertion type.
This aligns the documentation with the actual code behavior referenced in
plugin.go where jwt-bearer is noted as being for Okta compatibility only, and
there are no verified tests demonstrating Keycloak's jwt-bearer support.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d04e6ef4-f0bc-4a70-8bc4-f151cf2347c7

📥 Commits

Reviewing files that changed from the base of the PR and between e76ffe8 and f40ce79.

📒 Files selected for processing (6)
  • authbridge/authlib/plugins/tokenexchange/plugin.go
  • authbridge/authlib/plugins/tokenexchange/plugin_test.go
  • authbridge/authlib/plugins/tokenexchange/provider.go
  • authbridge/authlib/plugins/tokenexchange/provider_keycloak.go
  • authbridge/authlib/plugins/tokenexchange/provider_test.go
  • authbridge/docs/idp-plugin-contract.md

Comment thread authbridge/authlib/plugins/tokenexchange/plugin.go Outdated
Comment thread authbridge/authlib/plugins/tokenexchange/plugin.go Outdated
Comment thread authbridge/docs/idp-plugin-contract.md Outdated

@huang195 huang195 left a comment

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.

Solid, well-tested Phase-1 config generalization with a clean, backward-compatible deprecation path (keycloak_url/keycloak_realmprovider_*). The direction — isolate IdP-specifics, keep the RFC 8693 core generic — is right.

My main concern is that the abstraction draws its boundary around the least divergent axis (endpoint URL templating) while the genuinely IdP-specific axis (client authentication) stays as loose, uncoordinated config flags outside the IdPProvider interface. Entra ID needs certificate auth (private_key_jwt/x5t) and Okta needs jwt-bearer — per this PR's own Phase 2/3 plan, both require touching exchange.ClientAuth + buildClientAuthFrom. So the headline "add one provider_<name>.go, no core changes" promise holds only for URLs, not for the auth those IdPs actually require. Widening IdPProvider to own auth-method selection + defaults + validation (see inline comments) would make it deliver on that promise and reject invalid combinations at config time instead of at the IdP.

One must-fix: the committed design doc (idp-plugin-contract.md) describes a different design (a switch/resolveEndpoints()) than the shipped code (interface + registry), and its Phase-1 checklist marks resolveEndpoints() as done — it doesn't exist. That will mislead the next contributor; please reconcile it with the as-built code.

The Keycloak path itself, backward compat, and test coverage all look good — this is about getting the extension shape right before more IdPs are built on it.

Author: akram (MEMBER — maintainer) · Areas reviewed: Go, Docs · Agent/IDE config (.claude/.vscode): none · Commits: 1, signed-off: yes · CI: passing

Assisted-By: Claude Code

Comment thread authbridge/docs/idp-plugin-contract.md Outdated

### Interface (Go)

No new Go interface is needed for URL derivation — it is a pure

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.

must-fix — This contradicts the shipped code. The section says "No new Go interface is needed for URL derivation… Implemented as a switch in applyDefaults()" and gives a resolveEndpoints() function (line 124); the Phase-1 checklist (line 246) lists "Implement resolveEndpoints()" as done. But the PR actually ships an IdPProvider interface + registry, and resolveEndpoints() does not exist. A contributor following this doc would build the wrong thing. Please reconcile the contract with the as-built design (the IdPProvider interface) before merge.

Comment thread authbridge/authlib/plugins/tokenexchange/provider.go
// JWKSEndpoint derives the JWKS endpoint URL from the provider
// base URL and realm/tenant. Returns "" if the inputs are
// insufficient (caller must supply explicit jwks_url).
JWKSEndpoint(providerURL, providerRealm string) string

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.

suggestionJWKSEndpoint() has no consumer on the outbound exchange path: applyDefaults() only calls TokenEndpoint(). JWKS is for validating inbound tokens (the jwtvalidation plugin's job), not the exchange client. As-is it's interface surface every future provider must implement for no current effect. Recommend dropping it from this interface (and its test) until something consumes it.

// which leaves TokenURL empty — validate() will catch it.
if c.TokenURL == "" && c.Provider != "" {
if p := LookupProvider(c.Provider); p != nil {
c.TokenURL = p.TokenEndpoint(c.ProviderURL, c.ProviderRealm)

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.

suggestion — Pair the derivation with provider-driven defaults + validation. After deriving TokenURL, set c.Identity.AssertionType = p.DefaultAssertionType() when unset (so Okta gets jwt-bearer automatically without the operator knowing the URN), and reject an identity.type that isn't in p.SupportedAuthMethods(). Today nothing ties provider / identity.type / assertion_type together — provider: okta + jwt-spiffe is accepted here even though your own matrix (doc line 723) says Okta rejects it, and it only fails later at the IdP rather than at Configure().

}
urn := assertionTypeURN[assertionType]
if urn == "" {
urn = assertionTypeURN["jwt-spiffe"] // default

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.

suggestion — This hardcoded jwt-spiffe fallback is what silently makes every IdP default to Keycloak's assertion URN. If the provider supplies the default (see comment on applyDefaults), this should source from p.DefaultAssertionType() rather than being pinned to jwt-spiffe here — otherwise an Okta config that omits assertion_type silently gets the wrong (Keycloak) URN.

// When empty and keycloak_url is set, defaults to "keycloak" for
// backward compatibility. When "generic" or empty, token_url must
// be supplied explicitly.
Provider string `json:"provider" description:"IdP provider for endpoint derivation: keycloak, entra-id, okta, or generic." enum:"keycloak,entra-id,okta,generic"`

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.

suggestion — The enum advertises entra-id and okta, but only keycloak is registered in this PR. Selecting them makes LookupProvider return nil → TokenURL stays empty → the user hits a confusing "token_url required" error instead of "provider not implemented." Recommend trimming the enum to the implemented providers (keycloak, generic) until #481 Phases 2/3 land, then re-adding each as it ships.

@akram

akram commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @huang195 , I will address the comments.

akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
Introduce an IdPProvider interface and provider registry so new IdP
backends (Entra ID, Okta, etc.) can be added without modifying core
plugin code. Each provider implements TokenEndpoint() and JWKSEndpoint()
for its URL conventions and registers via init().

Changes:
- IdPProvider interface with TokenEndpoint/JWKSEndpoint contract
- Provider registry (RegisterProvider/LookupProvider) in provider.go
- Keycloak provider as built-in reference implementation
- Config fields: provider, provider_url, provider_realm for IdP selection
- Configurable assertion_type on spiffe identity (jwt-spiffe/jwt-bearer)
- Backward compat: keycloak_url/keycloak_realm auto-migrate with warning
- Contract documented in docs/idp-plugin-contract.md

To add a new IdP (e.g. Entra ID, Okta):
  1. Implement IdPProvider (Name, TokenEndpoint, JWKSEndpoint)
  2. Call RegisterProvider() in an init() function
  3. No changes to plugin.go needed

Ref: RHAIENG-5681
Upstream: kagenti-extensions#481

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram
akram force-pushed the feat/idp-agnostic-token-exchange branch from f40ce79 to 05c0f7a Compare June 18, 2026 12:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
authbridge/docs/idp-plugin-contract.md (2)

209-212: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the jwt-bearer support row.

Keycloak is still marked as supporting jwt-bearer, but the current implementation/docs only verify jwt-spiffe for Keycloak and route jwt-bearer to Okta. Please change this row so the matrix reflects what is actually supported.

Based on learnings: this mismatch was already called out in a previous review comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/docs/idp-plugin-contract.md` around lines 209 - 212, The support
matrix in the idp-plugin-contract.md documentation incorrectly shows Keycloak as
supporting the jwt-bearer assertion type with a checkmark, but the actual
implementation only verifies jwt-spiffe for Keycloak and routes jwt-bearer
assertions to Okta. Update the jwt-bearer row to change Keycloak's status from ✅
to ❌ so the matrix accurately reflects the current implementation behavior.

116-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reconcile this section with the registry-based design.

This still describes a switch-based resolveEndpoints() helper, but the PR ships an IdPProvider interface plus RegisterProvider()/LookupProvider(). Please rewrite this section and the Phase 1 checklist so the contract matches the actual implementation.

Based on learnings: the PR summary says endpoint derivation is implemented through a provider registry.

Also applies to: 243-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/docs/idp-plugin-contract.md` around lines 116 - 147, The
documentation section describes endpoint derivation using a switch-based
resolveEndpoints() function, but the actual PR implementation uses a
registry-based design with an IdPProvider interface and
RegisterProvider()/LookupProvider() functions. Rewrite this section to
accurately describe the registry pattern: explain how providers are registered
using the IdPProvider interface, how endpoint URLs are derived through the
registry lookup mechanism, and how the provider registry enables extensibility.
Update the Phase 1 checklist and the section at lines 243-249 to reflect this
registry-based approach rather than the switch-based implementation shown in the
code example.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/authlib/plugins/tokenexchange/plugin_test.go`:
- Around line 399-405: In the TestKeycloakProvider_TokenEndpoint_TrailingSlash
function and similar test functions mentioned (lines 408-421, 423-465), add an
explicit nil check immediately after calling LookupProvider("keycloak") on the
variable p before attempting to call methods on it. If p is nil, use t.Fatal or
t.Fatalf to fail the test with a clear error message rather than allowing the
test to panic. Apply this same nil guard pattern to all test functions that call
LookupProvider and dereference its result.
- Around line 415-420: The test TestKeycloakProvider_SupportedIdentityTypes is
checking the supported identity types at hard-coded slice indices (supported[0]
and supported[1]), which is brittle and will fail if the order changes even
though the provider still supports the correct types. Modify the assertion to be
order-independent by checking that the returned slice contains both
"client-secret" and "spiffe" values regardless of their position in the slice,
such as by checking membership in the slice without relying on specific indices.

In `@authbridge/docs/idp-plugin-contract.md`:
- Around line 15-17: The fenced code blocks in the documentation are missing
language specifiers after the opening triple backticks, which violates MD040
markdown linting rules. Add a language label such as `text` or `plaintext`
immediately after each opening ``` fence throughout the document. This applies
to the diagram block showing the request flow, the component descriptions block,
and any other bare fenced blocks to ensure consistent rendering and maintain
markdown lint compliance.

---

Duplicate comments:
In `@authbridge/docs/idp-plugin-contract.md`:
- Around line 209-212: The support matrix in the idp-plugin-contract.md
documentation incorrectly shows Keycloak as supporting the jwt-bearer assertion
type with a checkmark, but the actual implementation only verifies jwt-spiffe
for Keycloak and routes jwt-bearer assertions to Okta. Update the jwt-bearer row
to change Keycloak's status from ✅ to ❌ so the matrix accurately reflects the
current implementation behavior.
- Around line 116-147: The documentation section describes endpoint derivation
using a switch-based resolveEndpoints() function, but the actual PR
implementation uses a registry-based design with an IdPProvider interface and
RegisterProvider()/LookupProvider() functions. Rewrite this section to
accurately describe the registry pattern: explain how providers are registered
using the IdPProvider interface, how endpoint URLs are derived through the
registry lookup mechanism, and how the provider registry enables extensibility.
Update the Phase 1 checklist and the section at lines 243-249 to reflect this
registry-based approach rather than the switch-based implementation shown in the
code example.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d48a3bef-5234-464b-8696-a70f177e6c8f

📥 Commits

Reviewing files that changed from the base of the PR and between f40ce79 and 05c0f7a.

📒 Files selected for processing (6)
  • authbridge/authlib/plugins/tokenexchange/plugin.go
  • authbridge/authlib/plugins/tokenexchange/plugin_test.go
  • authbridge/authlib/plugins/tokenexchange/provider.go
  • authbridge/authlib/plugins/tokenexchange/provider_keycloak.go
  • authbridge/authlib/plugins/tokenexchange/provider_test.go
  • authbridge/docs/idp-plugin-contract.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • authbridge/authlib/plugins/tokenexchange/provider_test.go
  • authbridge/authlib/plugins/tokenexchange/plugin.go

Comment thread authbridge/authlib/plugins/tokenexchange/plugin_test.go
Comment thread authbridge/authlib/plugins/tokenexchange/plugin_test.go
Comment thread authbridge/docs/idp-plugin-contract.md Outdated
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram
akram force-pushed the feat/idp-agnostic-token-exchange branch from 05c0f7a to 317bd14 Compare June 18, 2026 13:05
akram added a commit to akram/kagenti-extensions that referenced this pull request Jun 18, 2026
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram
akram force-pushed the feat/idp-agnostic-token-exchange branch from 317bd14 to 5468fde Compare June 18, 2026 13:11
Per review feedback on PR rossoctl#521, the IdPProvider interface now covers
the full IdP surface (endpoint derivation + client authentication),
not just URL templating.

Changes:
- IdPProvider interface: add DefaultAssertionType(),
  SupportedIdentityTypes(), BuildClientAuth() — remove JWKSEndpoint()
  (no consumer on the exchange path, belongs to jwt-validation)
- Keycloak provider: implements full interface including auth building
- Remove buildClientAuthFrom() from core — delegated to provider
- Validate provider↔identity compatibility at Configure() time
  (e.g. provider:keycloak + identity.type:certificate → rejected)
- Reject unknown assertion_type values (was silently defaulting)
- Reject unknown provider names at validation
- Trim provider enum to implemented providers only (keycloak, generic)
- Fix mixed back-compat path (keycloak_realm without keycloak_url)
- IdentityConfig exported struct for provider contract

Tests:
- 5 new Keycloak provider tests (DefaultAssertionType,
  SupportedIdentityTypes, BuildClientAuth for client-secret/spiffe,
  unsupported type rejection)
- 3 new validation tests (invalid assertion_type, provider↔identity
  incompatibility, unknown provider rejection)
- All 40 plugin tests pass, full suite zero regression

Ref: RHAIENG-5681

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram
akram force-pushed the feat/idp-agnostic-token-exchange branch from 5468fde to f904c2f Compare June 18, 2026 13:13
@akram
akram requested a review from huang195 June 18, 2026 13:17

@huang195 huang195 left a comment

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.

Excellent turnaround — the updated commit (f904c2f) resolves all six items from the prior review, including the must-fix doc/code mismatch.

  • Doc reconciledidp-plugin-contract.md now documents the actual 5-method IdPProvider interface; the contradictory switch/resolveEndpoints() section is gone and status is "Implemented."
  • Interface widenedIdPProvider now owns the full per-IdP surface: TokenEndpoint, DefaultAssertionType, SupportedIdentityTypes, and BuildClientAuth. This is the right boundary: adding Okta is a genuine one-file drop-in, and Entra ID is one provider file plus a shared CertificateAuth.
  • JWKSEndpoint removed from the interface.
  • Config-time validationvalidate() now rejects unknown providers, invalid assertion_type, and provider↔identity-type incompatibilities (e.g. keycloak + certificate), instead of failing later at the IdP.
  • Default assertion type is now provider-driven (applyDefaults pulls DefaultAssertionType()); the hardcoded jwt-spiffe URN is gone.
  • Enum trimmed to keycloak,generic.

New tests track each behavior (BuildClientAuth_*, Validate_InvalidAssertionType, Validate_ProviderIdentityIncompatibility, Validate_UnknownProviderRejected), and pollCredentials correctly threads provider through on credential reload. CI is green.

One non-blocking nit inline. Optional future cleanup (not needed here): generic is handled as an inline fallback in buildClientAuth that duplicates the Keycloak spiffe/client-secret logic — registering a genericProvider would collapse it to a single LookupProvider(name).BuildClientAuth(...) path and drop the generic special-casing in validate().

Author: akram (MEMBER — maintainer) · Areas reviewed: Go, Docs · Agent/IDE config (.claude/.vscode): none · Commits: 2, signed-off: yes · CI: passing

Assisted-By: Claude Code

TokenURL string `json:"token_url" description:"OAuth token endpoint URL. Required unless provider + provider_url (or keycloak_url + keycloak_realm) are set for automatic derivation."`

// Provider selects the IdP for automatic endpoint derivation.
// Supported: keycloak, entra-id, okta, generic.

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.

nit — This comment still says "Supported: keycloak, entra-id, okta, generic" (and the provider_url/provider_realm comments below at lines 46-53 describe entra-id/okta interpretations), but the enum is now correctly keycloak,generic. Minor staleness — either trim the comment to match the enum, or mark the not-yet-registered ones as "(future)" so it doesn't read as currently supported.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants