CVE-2025-1391
Description
A flaw was found in the Keycloak organization feature, which allows the incorrect assignment of an organization to a user if their username or email matches the organization’s domain pattern. This issue occurs at the mapper level, leading to misrepresentation in tokens. If an application relies on these claims for authorization, it may incorrectly assume a user belongs to an organization they are not a member of, potentially granting unauthorized access or privileges.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | >= 26.1.0, < 26.1.3 | 26.1.3 |
org.keycloak:keycloak-servicesMaven | < 26.0.10 | 26.0.10 |
Patches
15aa2b4c75bb4Only set organization to client session when re-authenticating if user is member of the mapped organization
4 files changed · +77 −5
services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java+1 −1 modified@@ -163,7 +163,7 @@ private OrganizationModel resolveOrganization(UserModel user, String domain) { if (alias.isEmpty()) { OrganizationModel organization = Organizations.resolveOrganization(session, user, domain); - if (organization != null) { + if (isSSOAuthentication(authSession) && organization != null) { // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); }
services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java+5 −4 modified@@ -36,6 +36,7 @@ import org.keycloak.models.OrganizationModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; @@ -122,8 +123,8 @@ protected void setClaim(IDToken token, ProtocolMapperModel model, UserSessionMod KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); ProtocolMapperModel effectiveModel = getEffectiveModel(session, realm, model); - - Object claim = resolveValue(effectiveModel, organizations.toList()); + UserModel user = userSession.getUser(); + Object claim = resolveValue(effectiveModel, user, organizations.toList()); if (claim == null) { return; @@ -144,7 +145,7 @@ private Stream<OrganizationModel> resolveFromRequestedScopes(KeycloakSession ses } - private Object resolveValue(ProtocolMapperModel model, List<OrganizationModel> organizations) { + private Object resolveValue(ProtocolMapperModel model, UserModel user, List<OrganizationModel> organizations) { if (organizations.isEmpty()) { return null; } @@ -156,7 +157,7 @@ private Object resolveValue(ProtocolMapperModel model, List<OrganizationModel> o Map<String, Map<String, Object>> value = new HashMap<>(); for (OrganizationModel o : organizations) { - if (o == null || !o.isEnabled()) { + if (o == null || !o.isEnabled() || user == null || !o.isMember(user)) { continue; }
services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java+4 −0 modified@@ -736,6 +736,10 @@ private static ClientScopeModel tryResolveDynamicClientScope(KeycloakSession ses return null; } + if (user != null && orgScope.resolveOrganizations(user, scopeParam, session).findAny().isEmpty()) { + return null; + } + return orgScope.toClientScope(name, user, session); }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java+67 −0 modified@@ -861,6 +861,53 @@ public void testCustomOrganizationScopeName() { assertScopeAndClaims(scopeName, orgA); } + @Test + public void testClaimNotMappedIfUserNotMemberWhenDefaultClientScope() { + OrganizationRepresentation orgARep = createOrganization("orga", true); + OrganizationResource orgA = testRealm().organizations().get(orgARep.getId()); + MemberRepresentation member = addMember(orgA, "member@" + orgARep.getDomains().iterator().next().getName()); + orgA.members().member(member.getId()).delete().close(); + + ClientRepresentation clientRep = testRealm().clients().findByClientId("broker-app").get(0); + ClientResource client = testRealm().clients().get(clientRep.getId()); + ClientScopeRepresentation orgScopeRep = client.getOptionalClientScopes().stream().filter(scope -> "organization".equals(scope.getName())).findAny().orElse(null); + client.removeOptionalClientScope(orgScopeRep.getId()); + client.addDefaultClientScope(orgScopeRep.getId()); + getCleanup().addCleanup(() -> { + client.removeDefaultClientScope(orgScopeRep.getId()); + client.addOptionalClientScope(orgScopeRep.getId()); + }); + // resolve organization based on the organization scope value + oauth.clientId("broker-app"); + oauth.scope(null); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + loginPage.login(memberPassword); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + assertThat(response.getScope(), containsString(orgScopeRep.getName())); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), containsString(orgScopeRep.getName())); + assertThat(accessToken.getOtherClaims().keySet(), not(hasItem(OAuth2Constants.ORGANIZATION))); + } + + @Test + public void testClaimNotMappedIfUserNotMemberWhenScopeOrgAliasRequested() { + OrganizationRepresentation orgARep = createOrganization("orga", true); + assertClaimNotMapped("organization:" + orgARep.getAlias(), orgARep, false); + } + + @Test + public void testClaimNotMappedIfUserNotMemberWhenScopeOrgAllRequested() { + assertClaimNotMapped("organization:*", createOrganization("orga", true), false); + } + + @Test + public void testClaimNotMappedIfUserNotMemberWhenScopeOrgRequested() { + assertClaimNotMapped("organization", createOrganization("orga", true), true); + } + private AccessTokenResponse assertSuccessfulCodeGrant() { String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); @@ -926,4 +973,24 @@ private void setMapperConfig(String key, String value) { orgScopeResource.getProtocolMappers().update(orgMapper.getId(), orgMapper); } + + private void assertClaimNotMapped(String orgScope, OrganizationRepresentation orgARep, boolean grantScope) { + OrganizationResource orgA = testRealm().organizations().get(orgARep.getId()); + MemberRepresentation member = addMember(orgA, "member@" + orgARep.getDomains().iterator().next().getName()); + orgA.members().member(member.getId()).delete().close(); + driver.manage().timeouts().pageLoadTimeout(Duration.ofDays(1)); + // resolve organization based on the organization scope value + oauth.clientId("broker-app"); + oauth.scope(orgScope); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + loginPage.login(memberPassword); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse response = oauth.doAccessTokenRequest(code, KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + assertThat(response.getScope(), grantScope ? containsString(orgScope) : not(containsString(orgScope))); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + assertThat(accessToken.getScope(), grantScope ? containsString(orgScope) : not(containsString(orgScope))); + assertThat(accessToken.getOtherClaims().keySet(), not(hasItem(OAuth2Constants.ORGANIZATION))); + } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-gvgg-2r3r-53x7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-1391ghsaADVISORY
- access.redhat.com/errata/RHSA-2025:2545nvdWEB
- access.redhat.com/security/cve/CVE-2025-1391nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/5aa2b4c75bb474303ab807017582bc01a9f7e378ghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-gvgg-2r3r-53x7ghsaWEB
- access.redhat.com/errata/RHSA-2025:2544nvd
- github.com/keycloak/keycloak/issues/37169nvd
- github.com/keycloak/keycloak/pull/37235nvd
News mentions
0No linked articles in our index yet.