8000 Re-validate stored client URIs on partial client updates in the secure-client-uris executors by choksi2212 · Pull Request #53992 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Re-validate stored client URIs on partial client updates in the secure-client-uris executors - #53992

Open
choksi2212 wants to merge 2 commits into
keycloak:mainfrom
choksi2212:fix/secure-client-uris-partial-update-53952
Open

choksi2212 wants to merge 2 commits into
keycloak:mainfrom
choksi2212:fix/secure-client-uris-partial-update-53952

Conversation

@choksi2212
Copy link
Copy Markdown
Contributor

Description

The secure-client-uris and secure-client-uris-pattern executors validate the URIs of the proposed ClientRepresentation only. A client update issued through the Admin REST API is a partial update: it carries only the changed fields, and the fields absent from the payload keep their stored values (see RepresentationToModel.updateClientProperties). As a result, the URI fields (redirectUris, adminUrl, baseUrl, jwksUrl and the remaining validated fields) that are omitted from the update payload are never re-validated — a client that holds non-compliant values (for example because it was created before the policy was enabled) can be updated indefinitely without the policy noticing, which is the gap #53952 describes for the UPDATE operation.

Both executors now validate the effective client state on UPDATE: the proposed representation merged with the stored client. The merge helper (SecureClientUrisExecutor.effectiveClientRepresentation) mirrors the merge semantics of RepresentationToModel.updateClientProperties exactly — first-non-null for the scalar URL fields, fallback to the stored redirectUris/webOrigins lists, and a per-key attribute merge that skips null values — the same idiom #53038 established for the register path, so the executors now judge the same client state that will be persisted. The helper is shared by both executors; a partial update that replaces a stored non-compliant URI with a compliant one is still accepted, because the merged view carries the new value.

Registration is unchanged: there is no stored client to merge with, so the proposed representation is validated as before.

New tests

SecureClientUrisPartialUpdateTest (tests/base) seeds a non-compliant URI field on the client before the policy is enabled, then issues a partial update (description-only) with the policy active, so the update can only be rejected by re-validating the stored client state:

  • testPartialUpdateRevalidatesStoredJwksUri, testPartialUpdateRevalidatesStoredAdminUrl, testPartialUpdateRevalidatesStoredRedirectUris — the partial update is rejected by secure-client-uris with invalid_client metadata errors (Invalid jwksUri / Invalid adminUrl / Invalid redirectUris).
  • testPatternPartialUpdateRevalidatesStoredJwksUri — the same contract for secure-client-uris-pattern with an allowedPatterns configuration that rejects the stored jwksUrl.
  • testPartialUpdateReplacingStoredJwksUriIsAccepted, testPatternPartialUpdateReplacingStoredJwksUriIsAccepted — partial updates that replace the stored non-compliant jwksUrl with a compliant one are accepted and persisted.

The four reject tests fail on current main with Expected ClientPolicyException to be thrown, but nothing was thrown — verified against a distribution carrying the pre-fix executors (tests kept): the 4 reject tests fail with exactly that signature while the 2 accept tests pass.

Fixes #53952

Checklist

  • All code changes were tested with a real Keycloak distribution: the full org.keycloak.tests.client.policies package in tests/base (including the new class) passes against a booted distribution built with this change.
  • There are no new warnings or errors.
  • Documentation is not required for this change: the admin docs already state the executors enforce secure URIs; this closes the gap between that documented behavior and the partial-update path.
  • Each commit has a valid Signed-off-by: choksi2212 <choksi2212@users.noreply.github.com> trailer (DCO).

Note on AI usage: This pull request was prepared with the assistance of an AI agent (Hermes by Nous Research) generating the complete solution from a prompt, per the contributing guidelines on generative AI. Every change was reviewed, tested, and verified against a real Keycloak distribution by the author.

…e-client-uris executors

Both the secure-client-uris and the secure-client-uris-pattern executors
validated only the proposed ClientRepresentation on updates. A client
update issued through the Admin REST API is a partial update: fields
absent from the payload keep their stored values (see
RepresentationToModel.updateClientProperties), so the URI fields omitted
from the update payload were never re-validated, and a client holding
non-compliant redirectUris/adminUrl/jwksUrl values - for example because
it was created before the policy was enabled - could be updated
indefinitely without the policy rejecting it.

Validate the effective client state on UPDATE instead: the proposed
representation merged with the stored client, with the same merge
semantics as RepresentationToModel.updateClientProperties (first-non-null
scalars, list fallback, per-key attribute merge skipping null values) -
the same idiom keycloak#53038 established for the register path. The shared
helper lives on SecureClientUrisExecutor and is reused by
SecureClientUrisPatternExecutor. Partial updates that replace a stored
non-compliant URI with a compliant one remain accepted; registration is
unchanged as there is no stored client to merge with.

Fixes keycloak#53952

Signed-off-by: choksi2212 <choksi2212@users.noreply.github.com>
@choksi2212
choksi2212 requested a review from a team as a code owner October 15, 2026 00:47
Copilot AI balanced review requested due to automatic review settings October 15, 2026 00:47
Copilot AI 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.

🟡 Changes recommended

Null-attribute deletions and derived web origins are merged differently from persistence, causing incorrect validation.

2 open findings
What changed in this PR

Addresses #53952 by revalidating stored client URIs during partial updates in both URI policy executors, while leaving registration unchanged.

Changes:

  • Adds a shared effective-state merge for update validation.
  • Applies it to both URI policy executors.
  • Adds six regression tests covering rejection and valid URI replacement.
File Description
tests/​base/​src/​test/​java/​org/​keycloak/​tests/​client/​policies/​SecureClientUrisPartialUpdateTest.java Tests partial-update validation and URI replacement.
services/​src/​main/​java/​org/​keycloak/​services/​clientpolicy/​executor/​SecureClientUrisPatternExecutor.java Validates merged state on updates.
services/​src/​main/​java/​org/​keycloak/​services/​clientpolicy/​executor/​SecureClientUrisExecutor.java Adds shared merge logic and uses it on updates.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…ins in the effective client representation

An attribute set to null (or to an empty value) during a client update removes the
stored key (see ClientAdapter#setAttribute), so the effective representation must not
keep the stale URI - otherwise a valid remediation like {"attributes":{"jwks.url":null}}
is rejected by the secure-client-uris executors. Web origins omitted from an update of a
client without stored web origins are derived from the redirect URIs effective after the
update (see RepresentationToModel#defaultWebOrigins), so the derived origins are validated
as well; a pattern policy checking only webOrigins could otherwise be bypassed by
updating redirectUris alone.

Covered by the new SecureClientUrisExecutorTest (the explicit null case is a raw-HTTP-only
payload since the admin client drops null attribute values, JsonInclude.NON_NULL) and by
the webOrigins derivation plus empty-value deletion tests in the testsuite.

Signed-off-by: choksi2212 <choksi2212@users.noreply.github.com>
Copilot AI 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.

🟢 Approval recommended

No blocking issues remain, and regression tests cover stored-state validation and remediation.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

@choksi2212
choksi2212 force-pushed the fix/secure-client-uris-partial-update-53952 branch from a076c7f to c6e8d5a Compare October 15, 2026 02:45
@choksi2212
Copy link
Copy Markdown
Contributor Author

Both findings verified against the persistence code and fixed in c6e8d5a - thanks for catching these, they were real semantic gaps in the merge.

Finding 1 (web origins derived from redirect URIs on omit). Confirmed: RepresentationToModel#updateClientProperties (server-spi-private) applies the redirect-URIs update before the web-origins update, and its fallback defaultWebOrigins(client) derives origins from the already-updated redirect URIs whenever the stored set is empty - so a webOrigins-only pattern policy could indeed be bypassed by updating redirectUris alone. The merge now mirrors this via SecureClientUrisExecutor#effectiveWebOrigins: proposed web origins win, a non-empty stored set is preserved, and an omitted field on an empty stored set derives from the effective redirect URIs exactly like defaultWebOrigins (http-filtered, UriUtils::getOrigin, set semantics). Regression tests in the testsuite: testPatternPartialUpdateDerivesWebOriginsFromUpdatedRedirectUris (rejects an update to an untrusted host that would persist the derived origin http://untrusted.example.com) and testPatternPartialUpdateDerivedWebOriginsMatchingPatternAreAccepted (accepts a trusted-host update and pins that the derived origin is actually persisted). Negative control on the previous head: the reject test fails with Expected ClientPolicyException to be thrown, but nothing was thrown - the exact old-behavior signature - while both pass on the fixed build.

Finding 2 (explicit null attribute values are deletions). Confirmed: model/jpa/ClientAdapter#setAttribute treats null (and empty/blank values) as removeAttribute(name), so {"attributes":{"jwks.url":null}} is a valid remediation the merge wrongly rejected. The merge now removes explicitly null (or empty/blank) attribute keys instead of skipping them, mirroring ClientAdapter#setAttribute's valueUndefined check. One implementation note for the test coverage: the admin client does not serialize null attribute values at all (JsonInclude.NON_NULL in both JacksonProvider and Jackson3MapperHolder), so an explicit null can only reach the server from a raw HTTP update - the null case is therefore pinned by the new SecureClientUrisExecutorTest (null, empty and blank values remove the stored key; replaced values win; omitted fields keep stored values; plus the web-origins derivation and explicit-clear cases), while the deletion semantics are additionally covered end-to-end by testPartialUpdateDeletingStoredJwksUriByEmptyValueIsAccepted, which verifies through the running server that the empty-value update is accepted and the attribute is actually gone afterwards. Negative control on the previous head: the null-deletion tests fail with ClientPolicyException: invalid_client_metadata (valid remediation rejected), and the corresponding unit tests fail with expected null, but was:<http://stored.example.com/jwks>; all green on the fixed build.

Verification: 9/9 testsuite tests and 8/8 unit tests green on the fixed build (full services -> quarkus/server -> quarkus/dist rebuild, tests run against the real distribution server); sibling classes unaffected: ClientUrisPatternExecutorTest 5/5, SecureClientNodeExecutorTest 20/20, AdminProtocolMapperClientPolicyTest 13/13, ClientPoliciesClientCRUDTest 3/3; spotless clean.

@choksi2212
Copy link
Copy Markdown
Contributor Author
8000

CI triage for the red Java Distribution IT/UT (ubuntu-latest, temurin, 27) job: the only error is JDKTestSuite.listApplicationsWithoutPermission » IllegalState: Server did not become ready within 120 seconds (241 tests run, 0 failures, 1 boot timeout — no assertion failures anywhere).

Three data points say this is the distribution-matrix boot flake, not this change:

  1. The same run boots the same suite fine on windows-latest, temurin, 27 (same JDK, same code) and on ubuntu-latest, temurin, 17 and temurin, 21, plus windows temurin, 17/21 — all green.
  2. Every later suite in the same job (MutualTLSClientTest, AuthorizationCodeTest, LoginTest, X509BrowserLoginTest, …) started its managed server and passed normally after the timeout.
  3. The same job class failed on unrelated branches around the same window — e.g. Java Distribution IT/UT (windows-latest, temurin, 17) on release/26.8 (run 38593334199, which does not contain this change).

The Status Check - Keycloak CI failure is just the aggregate following that one job. Could someone with permissions re-run the temurin-27 job? If it turns out reproducible I will of course dig in properly.

@jimmychakkalakal jimmychakkalakal 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.

Thanks @choksi2212 for the contribution!

Reviewed both executors: client state validation for UPDATE events (including partial updates with omitted URI fields), replacements, attribute deletions, and derived web-origin handling all look good. No blocking issues found.

The JDKTestSuite failure looks like an unrelated server-readiness timeout. I approve the PR from my end.

@jimmychakkalakal
Copy link
Copy Markdown
Contributor

Hi @mposolda, I've reviewed and approved the PR from my end. The JDKTestSuite failure in the pipeline looks like an unrelated server-readiness timeout. Could you please trigger a rerun and take a look when you get a chance?

@choksi2212
8000
Copy link
Copy Markdown
Contributor Author

Thanks @jimmychakkalakal! On the JDKTestSuite failure: I triaged that run earlier (#53992 (comment)) — it's the unrelated temurin-27 server-readiness timeout, with no code-path connection to this change. @mposolda the rerun request details are there; happy to act on anything else that comes up.

@mposolda mposolda self-assigned this Oct 27, 2026
@mposolda mposolda 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.

@choksi2212 Thanks for the PR. There was a related fix pushed in the meantime, which seems also the cause of the conflict ae8fa4c . Can you please:

  • Confirm if the bug is still valid (even after ae8fa4c commit)
  • If it is still valid, can you rebase this PR on top of latest main to fix the conflict?

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.

Secure client URI executors do not re-validate stored URIs on partial client update

4 participants

0