From 73b60cc239030323c52856e218fc87a5e7ad2ec6 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 881aa2206656..79205223b0a4 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
@@ -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);
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 09f036a67c59..478822cc68a1 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
@@ -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())
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 5de881b75365..5b91bd3ae915 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
@@ -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());
+ }
}