Skip to content

Fix ConcurrentModificationException in client-registration policy reads#50969

Merged
ahus1 merged 1 commit into
keycloak:release/26.6from
ahus1:is-50286-backport-26.6
Jul 20, 2026
Merged

Fix ConcurrentModificationException in client-registration policy reads#50969
ahus1 merged 1 commit into
keycloak:release/26.6from
ahus1:is-50286-backport-26.6

Conversation

@ahus1

@ahus1 ahus1 commented Jul 16, 2026

Copy link
Copy Markdown
Member

The client-registration policies read their configuration with ComponentModel.getConfig().getList(key), which lazily inserts the key when it is absent. The ComponentModel is cached and shared across request threads, so concurrent dynamic client registrations structurally modify the same HashMap and one throws ConcurrentModificationException.

Read the config the same non-mutating way as the rest of the codebase, getConfig().getOrDefault(key, emptyList()), which keeps the empty-on-absent contract getList provided and removes the mutation. Applied to the TrustedHost, ClientScopes, ProtocolMappers and WebOrigins policies.

Closes #50286

The client-registration policies read their configuration with
ComponentModel.getConfig().getList(key), which lazily inserts the key
when it is absent. The ComponentModel is cached and shared across request
threads, so concurrent dynamic client registrations structurally modify
the same HashMap and one throws ConcurrentModificationException.

Read the config the same non-mutating way as the rest of the codebase,
getConfig().getOrDefault(key, emptyList()), which keeps the empty-on-absent
contract getList provided and removes the mutation. Applied to the
TrustedHost, ClientScopes, ProtocolMappers and WebOrigins policies.

Closes keycloak#50286

Signed-off-by: Michael Stingl <mail@michaelstingl.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
@ahus1 ahus1 self-assigned this Jul 16, 2026
@ahus1
ahus1 requested a review from pruivo July 16, 2026 20:50
@ahus1

ahus1 commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@pruivo - please review when you have the time. Thanks!

@keycloak-github-bot keycloak-github-bot 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.

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostSigTransientTest

Keycloak CI - Adapter IT Strict Cookies

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:87)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertTrue(Assert.java:53)
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.assertForbiddenLogin(SAMLServletAdapterTest.java:558)
...

Report flaky test

@ahus1
ahus1 marked this pull request as ready for review July 17, 2026 08:15
@ahus1
ahus1 requested a review from a team as a code owner July 17, 2026 08:15
@ahus1
ahus1 requested review from ruchikajha95 and removed request for pruivo July 17, 2026 14:49
if (allowedScopesConfig != null) {
allAllowed.addAll(allowedScopesConfig);
}
List<String> allowedScopesConfig = componentModel.getConfig().getOrDefault(ClientScopesClientRegistrationPolicyFactory.ALLOWED_CLIENT_SCOPES, Collections.emptyList());

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.

@ahus1 this changes the semantics. Is this ok?

  • getList - if the key does not exist, it creates a list, stores in the map, and keeps track of anything added to the list.
  • getOrDefault - if the key does not exist, it returns an unmodifiable list id

public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
List<String> allowedScopesConfig = config.getConfig().getList(ClientScopesClientRegistrationPolicyFactory.ALLOWED_CLIENT_SCOPES);
List<String> allowedScopesConfig = config.getConfig().getOrDefault(ClientScopesClientRegistrationPolicyFactory.ALLOWED_CLIENT_SCOPES, Collections.emptyList());
if (!getClientScopes(session).containsAll(allowedScopesConfig)) {

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.

getClientScopes(session).containsAll(allowedScopesConfig) is a O(N*M) complexity, where N is the size of getClientScopes list and M the size of allowedScopesConfig.
It may be problematic if these lists are large.

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

Since this is a backport, I'm ok to approve it as is.

@ahus1
ahus1 enabled auto-merge (squash) July 17, 2026 18:54
@ahus1
ahus1 removed the request for review from ruchikajha95 July 17, 2026 18:55

@rmartinc rmartinc 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 @ahus1!

@ahus1
ahus1 merged commit 245905f into keycloak:release/26.6 Jul 20, 2026
82 checks passed
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.

4 participants