VYPR
Medium severity6.5NVD Advisory· Published Jan 21, 2026· Updated Apr 15, 2026

CVE-2025-14559

CVE-2025-14559

Description

A flaw was found in the keycloak-services component of Keycloak. This vulnerability allows the issuance of access and refresh tokens for disabled users, leading to unauthorized use of previously revoked privileges, via a business logic vulnerability in the Token Exchange implementation when a privileged client invokes the token exchange flow.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.keycloak:keycloak-servicesMaven
>= 26.5.0, < 26.5.226.5.2
org.keycloak:keycloak-servicesMaven
< 26.4.926.4.9

Affected products

1

Patches

2
2d0aa31c4830

Check if requested user is enabled for impersonation in TE v1 (#45684)

https://github.com/keycloak/keycloakRicardo MartinJan 22, 2026via ghsa
3 files changed · +41 1
  • services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java+1 1 modified
    @@ -138,7 +138,7 @@ protected Response tokenExchange() {
                     requestedUser = session.users().getUserById(realm, requestedSubject);
                 }
     
    -            if (requestedUser == null) {
    +            if (requestedUser == null || !requestedUser.isEnabled()) {
                     // We always returned access denied to avoid username fishing
                     event.detail(Details.REASON, "requested_subject not found");
                     event.error(Errors.NOT_ALLOWED);
    
  • testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java+5 0 modified
    @@ -103,6 +103,11 @@ public UserAttributeUpdater setEmailVerified(Boolean emailVerified) {
             return this;
         }
     
    +    public UserAttributeUpdater setEnabled(Boolean enabled) {
    +        rep.setEnabled(enabled);
    +        return this;
    +    }
    +
         public UserAttributeUpdater setRequiredActions(UserModel.RequiredAction... requiredAction) {
             rep.setRequiredActions(Arrays.stream(requiredAction)
                     .map(action -> action.name())
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java+35 0 modified
    @@ -44,6 +44,7 @@
     import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
     import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
     import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
    +import org.keycloak.testsuite.updaters.UserAttributeUpdater;
     import org.keycloak.testsuite.util.AdminClientUtil;
     import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
     import org.keycloak.testsuite.util.oauth.OAuthClient;
    @@ -182,6 +183,24 @@ public void testImpersonation() throws Exception {
                 assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
             }
     
    +        // disabled user cannot be impersonated
    +        try (UserAttributeUpdater userUpdater = UserAttributeUpdater
    +                .forUserByUsername(adminClient.realm(TEST), "impersonated-user")
    +                .setEnabled(Boolean.FALSE)
    +                .update();
    +            Response response = exchangeUrl.request()
    +                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
    +                .post(Entity.form(
    +                            new Form()
    +                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
    +                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
    +                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
    +                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
    +                                    .param(OAuth2Constants.AUDIENCE, "target")
    +                        ))) {
    +            Assert.assertEquals(403, response.getStatus());
    +        }
    +
             try (Response response = exchangeUrl.request()
                     .post(Entity.form(
                             new Form()
    @@ -524,6 +543,22 @@ public void testDirectImpersonation() throws Exception {
                 assertTrue(response.getStatus() >= 400);
                 response.close();
             }
    +
    +        // disabled user cannot be impersonated
    +        try (UserAttributeUpdater userUpdater = UserAttributeUpdater
    +                .forUserByUsername(adminClient.realm(TEST), "impersonated-user")
    +                .setEnabled(Boolean.FALSE)
    +                .update();
    +            Response response = exchangeUrl.request()
    +                   .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
    +                   .post(Entity.form(
    +                            new Form()
    +                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
    +                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
    +                                    .param(OAuth2Constants.AUDIENCE, "target")
    +                    ))) {
    +            Assert.assertEquals(403, response.getStatus());
    +        }
         }
     
     
    
d67349f3aa9f

Check if requested user is enabled for impersonation in TE v1

https://github.com/keycloak/keycloakrmartincJan 21, 2026via ghsa
3 files changed · +41 1
  • services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java+1 1 modified
    @@ -140,7 +140,7 @@ protected Response tokenExchange() {
                     requestedUser = session.users().getUserById(realm, requestedSubject);
                 }
     
    -            if (requestedUser == null) {
    +            if (requestedUser == null || !requestedUser.isEnabled()) {
                     // We always returned access denied to avoid username fishing
                     event.detail(Details.REASON, "requested_subject not found");
                     event.error(Errors.NOT_ALLOWED);
    
  • testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java+5 0 modified
    @@ -106,6 +106,11 @@ public UserAttributeUpdater setEmailVerified(Boolean emailVerified) {
             return this;
         }
     
    +    public UserAttributeUpdater setEnabled(Boolean enabled) {
    +        rep.setEnabled(enabled);
    +        return this;
    +    }
    +
         public UserAttributeUpdater setRequiredActions(UserModel.RequiredAction... requiredAction) {
             rep.setRequiredActions(Arrays.stream(requiredAction)
                     .map(action -> action.name())
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java+35 0 modified
    @@ -42,6 +42,7 @@
     import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
     import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
     import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
    +import org.keycloak.testsuite.updaters.UserAttributeUpdater;
     import org.keycloak.testsuite.util.AdminClientUtil;
     import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
     import org.keycloak.testsuite.util.oauth.OAuthClient;
    @@ -185,6 +186,24 @@ public void testImpersonation() throws Exception {
                 assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
             }
     
    +        // disabled user cannot be impersonated
    +        try (UserAttributeUpdater userUpdater = UserAttributeUpdater
    +                .forUserByUsername(adminClient.realm(TEST), "impersonated-user")
    +                .setEnabled(Boolean.FALSE)
    +                .update();
    +            Response response = exchangeUrl.request()
    +                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
    +                .post(Entity.form(
    +                            new Form()
    +                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
    +                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
    +                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
    +                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
    +                                    .param(OAuth2Constants.AUDIENCE, "target")
    +                        ))) {
    +            Assert.assertEquals(403, response.getStatus());
    +        }
    +
             try (Response response = exchangeUrl.request()
                     .post(Entity.form(
                             new Form()
    @@ -527,6 +546,22 @@ public void testDirectImpersonation() throws Exception {
                 assertTrue(response.getStatus() >= 400);
                 response.close();
             }
    +
    +        // disabled user cannot be impersonated
    +        try (UserAttributeUpdater userUpdater = UserAttributeUpdater
    +                .forUserByUsername(adminClient.realm(TEST), "impersonated-user")
    +                .setEnabled(Boolean.FALSE)
    +                .update();
    +            Response response = exchangeUrl.request()
    +                   .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
    +                   .post(Entity.form(
    +                            new Form()
    +                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
    +                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
    +                                    .param(OAuth2Constants.AUDIENCE, "target")
    +                    ))) {
    +            Assert.assertEquals(403, response.getStatus());
    +        }
         }
     
     
    

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.