feat(plugins): Add keycloak_url + keycloak_realm to jwt-validation for internal JWKS derivation#383
Merged
Conversation
2 tasks
## Problem Pre-PR-rossoctl#378 the binary ran `config.Resolve()` → `deriveJWKSURL()` to derive `inbound.jwks_url` from `outbound.token_url` (which itself came from `keycloak_url` + `keycloak_realm` — the INTERNAL service URL). After the per-plugin cutover, each plugin decodes its config in isolation, so jwt-validation no longer sees token-exchange's fields. Its only fallback is to derive jwks_url from its own `issuer` — but `issuer` is the PUBLIC hostname that appears in token `iss` claims, which in split-horizon deployments (Kagenti's typical Kind and OpenShift setups) isn't reachable from inside the sidecar pod. Observed failure (CI run kagenti/kagenti#25525390807, weather-tool- advanced envoy-proxy stderr): msg="JWT validation failed" error="fetching JWKS: Get \"http://keycloak.localtest.me:8080/realms/kagenti /protocol/openid-connect/certs\": dial tcp [::1]:8080: connect: connection refused" Every inbound request → 401. Authbridge Weather (advanced) E2E broke. ## Fix Add `keycloak_url` + `keycloak_realm` fields to jwt-validation's config, symmetric with token-exchange's fields of the same name. `applyDefaults()` now derives `jwks_url` with this priority: 1. Explicit jwks_url wins 2. keycloak_url + keycloak_realm (the internal URL) 3. Issuer (single-horizon fallback — existing behavior) Chart/operator/backend callers pass `keycloak_url` + `keycloak_realm` that they already know; the plugin figures out the JWKS path. No one has to hand-roll the full URL anymore. ## Scope - `jwtvalidation.go` — two new fields, updated applyDefaults comment - `plugins_test.go` — 4 new tests + tightened existing JWKS test to assert the exact derived URL (so a future refactor of the priority chain can't silently fall through to a 404 path). Partial-keycloak fall-through is covered in both directions (url-without-realm and realm-without-url) to catch a future AND-check refactor. - `authbridge-combined.yaml` — pass KEYCLOAK_URL/KEYCLOAK_REALM into inbound jwt-validation as well as outbound token-exchange. Header comment explains where the env vars land from (envFrom on the Deployment, injected by operator/chart). ## Backward compatibility Purely additive: - Existing configs with only `issuer` still parse and work (single-horizon deployments unaffected). - Existing configs with explicit `jwks_url` still win (custom JWKS proxies unaffected). - New fields share namespace with token-exchange but decode independently — plugin configs remain isolated. ## Follow-ups Once this lands and a new extensions tag ships: - kagenti-operator: update `synthesizePipeline` to pass keycloak_url + keycloak_realm through to jwt-validation (it already has them in NamespaceConfig). Release a new operator alpha tag. - kagenti chart: drop the temporary explicit `jwks_url` added in kagenti#1507 once both new pins are available. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
huang195
force-pushed
the
feat/jwt-validation-keycloak-url
branch
from
May 7, 2026 23:29
e30bd6c to
76147af
Compare
pdettori
approved these changes
May 8, 2026
pdettori
left a comment
Member
There was a problem hiding this comment.
Clean, focused PR that restores split-horizon JWKS derivation lost during the per-plugin cutover in PR #378.
The three-tier priority logic in applyDefaults() is correct and well-tested (4 tests covering all paths including partial-config fallthrough). YAML template aligns with the Go struct fields.
Areas reviewed: Go, Tests, YAML, Security
Commits: 1 commit, signed-off ✓
CI: all passing (17/17 checks green)
No blocking issues found.
5 tasks
9 tasks
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
Per-plugin cutover (PR #378) silently dropped the cross-plugin derivation that let
inbound.jwks_urlbe derived fromoutbound.keycloak_url+keycloak_realm. Without that, jwt-validation falls back to deriving jwks_url fromissuer— which in split-horizon deployments is the public URL (required to match theissclaim bit-for-bit) and typically isn't reachable from inside the sidecar pod. Result: every inbound JWT validation hitsconnection refusedfetching the JWKS → 401.Observed failure in rossoctl/rossoctl#1507 CI, weather-tool-advanced envoy-proxy stderr:
This restores feature parity with pre-#378 semantics by giving jwt-validation its own
keycloak_url+keycloak_realmfields — symmetric with token-exchange, which already has them.Changes
authlib/plugins/jwtvalidation.go: two new config fields, updatedapplyDefaults()derivation priority:jwks_urlwinskeycloak_url+keycloak_realm→ internal URLissuer→ fallback (single-horizon; existing behavior)authlib/plugins/plugins_test.go: tightened existingDefaultsJWKSFromIssuerto assert exact URL; addedDerivesJWKSFromInternalKeycloakURL,ExplicitJWKSURLWins,PartialKeycloakConfigFallsThroughToIssuer.authproxy/authbridge-combined.yaml(image default): passKEYCLOAK_URL/KEYCLOAK_REALMinto inbound jwt-validation as well as outbound. Block comment explains split-horizon expectation.Pipeline config change
Before (chart/operator had to hand-roll the full URL):
After:
Both plugins now take the same shape for "where is Keycloak internally."
Backward compatibility
Purely additive:
issuerstill work (single-horizon).jwks_urlstill win (custom JWKS proxies).Follow-ups
synthesizePipelineto passkeycloak_url+keycloak_realmthrough to jwt-validation (already inNamespaceConfig). New alpha tag.jwks_urlworkaround once both new pins are available.Test plan
go test ./...inauthlib/— all green, including 3 new testsgo test ./...incmd/authbridge/— all greenAssisted-By: Claude (Anthropic AI) noreply@anthropic.com