From 68ac74d2f1ff397ba637c437ffb6451bef6cdefb Mon Sep 17 00:00:00 2001 From: rmartinc Date: Wed, 21 Jan 2026 17:13:15 +0100 Subject: [PATCH] Check if requested user is enabled for impersonation in TE v1 Closes #45651 Signed-off-by: rmartinc (cherry picked from commit d67349f3aa9fed5c61750619d0f9de6356aeaeff) --- .../V1TokenExchangeProvider.java | 2 +- .../updaters/UserAttributeUpdater.java | 5 +++ ...bjectImpersonationTokenExchangeV1Test.java | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java index 9d1f9218ed29..0eb3dc03b54b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java @@ -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); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java index 76bbe3c2bf0c..cc837526741c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java @@ -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()) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java index cd32a645ad62..4c55765a9814 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java @@ -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()); + } }