VYPR
Medium severity5.4NVD Advisory· Published Feb 17, 2025· Updated May 6, 2026

CVE-2025-1391

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.

PackageAffected versionsPatched versions
org.keycloak:keycloak-servicesMaven
>= 26.1.0, < 26.1.326.1.3
org.keycloak:keycloak-servicesMaven
< 26.0.1026.0.10

Patches

1
5aa2b4c75bb4

Only set organization to client session when re-authenticating if user is member of the mapped organization

https://github.com/keycloak/keycloakPedro IgorFeb 14, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.