diff --git a/server-spi-private/src/main/java/org/keycloak/models/ImpersonationSessionNote.java b/server-spi-private/src/main/java/org/keycloak/models/ImpersonationSessionNote.java
index 64621b7f1d..aea4393043 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/ImpersonationSessionNote.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/ImpersonationSessionNote.java
@@ -5,7 +5,8 @@ package org.keycloak.models;
*/
public enum ImpersonationSessionNote implements UserSessionNoteDescriptor {
IMPERSONATOR_ID("Impersonator User ID"),
- IMPERSONATOR_USERNAME("Impersonator Username");
+ IMPERSONATOR_USERNAME("Impersonator Username"),
+ IMPERSONATOR_CLIENT("Impersonator Client");
final String displayName;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
index ea7a429162..408f984862 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -23,9 +23,11 @@ import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureVerifierContext;
+import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.Urls;
import org.keycloak.util.JsonSerialization;
@@ -68,6 +70,21 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
}
}
}
+
+ String sessionState = accessToken.getSessionState();
+
+ if (sessionState != null) {
+ UserSessionModel userSession = session.sessions().getUserSession(realm, sessionState);
+
+ if (userSession != null) {
+ String actor = userSession.getNote(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString());
+
+ if (actor != null) {
+ // for token exchange delegation semantics when an entity (actor) other than the subject is the acting party to whom authority has been delegated
+ tokenMetadata.putObject("act").put("sub", actor);
+ }
+ }
+ }
} else {
tokenMetadata = JsonSerialization.createObjectNode();
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java
index d2e68d1607..7bf2b69036 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java
@@ -71,6 +71,7 @@ import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
+import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_CLIENT;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
@@ -339,7 +340,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
case OAuth2Constants.REFRESH_TOKEN_TYPE:
return exchangeClientToOIDCClient(targetUser, targetUserSession, requestedTokenType, targetClient, audience, scope);
case OAuth2Constants.SAML2_TOKEN_TYPE:
- return exchangeClientToSAML2Client(targetUser, targetUserSession, requestedTokenType, targetClient, audience, scope);
+ return exchangeClientToSAML2Client(targetUser, targetUserSession, requestedTokenType, targetClient);
}
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
@@ -393,6 +394,11 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
responseBuilder.getAccessToken().addAudience(audience);
}
+ if (formParams.containsKey(OAuth2Constants.REQUESTED_SUBJECT)) {
+ // if "impersonation", store the client that originated the impersonated user session
+ targetUserSession.setNote(IMPERSONATOR_CLIENT.toString(), client.getId());
+ }
+
if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)
&& OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
@@ -412,8 +418,7 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider {
return cors.builder(Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).build();
}
- protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType,
- ClientModel targetClient, String audience, String scope) {
+ protected Response exchangeClientToSAML2Client(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType, ClientModel targetClient) {
// Create authSession with target SAML 2.0 client and authenticated user
LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory()
.getProviderFactory(LoginProtocol.class, SamlProtocol.LOGIN_PROTOCOL);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 1cf98ecd51..15e6cbe856 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -45,6 +45,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
+import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -259,6 +260,13 @@ public class TokenManager {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
+ if (userSession == null) {
+ // also try to resolve sessions created during token exchange when the user is impersonated
+ userSession = session.sessions().getUserSessionWithPredicate(realm,
+ token.getSessionState(), false,
+ model -> client.getId().equals(model.getNote(ImpersonationSessionNote.IMPERSONATOR_CLIENT.toString())));
+ }
+
if (AuthenticationManager.isSessionValid(realm, userSession)) {
valid = isUserValid(session, realm, token, userSession.getUser());
} else {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java
index 1b698605ad..d0c11640db 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java
@@ -69,12 +69,17 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.util.JsonSerialization;
+
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+import com.fasterxml.jackson.databind.JsonNode;
+
/**
* @author Stian Thorgersen
*/
@@ -279,7 +284,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
{
response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret");
@@ -289,7 +294,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
- Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
}
{
@@ -301,7 +306,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
Assert.assertEquals("legal", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
- Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
}
{
response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
@@ -344,7 +349,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret");
String exchangedTokenString = response.getAccessToken();
@@ -353,7 +358,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
- Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
// can exchange to itself because the client is within the audience of the token issued to the public client
response = oauth.doTokenExchange(TEST, accessToken, null, "client-exchanger", "secret");
@@ -388,7 +393,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
// client-exchanger can impersonate from token "user" to user "impersonated-user"
{
@@ -445,7 +450,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
- Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
}
try (Response response = exchangeUrl.request()
@@ -496,6 +501,65 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
}
}
+ @Test
+ @UncaughtServerErrorExpected
+ public void testIntrospectTokenAfterImpersonation() throws Exception {
+ testingClient.server().run(ClientTokenExchangeTest::setupRealm);
+
+ oauth.realm(TEST);
+ oauth.clientId("client-exchanger");
+
+ Client httpClient = AdminClientUtil.createResteasyClient();
+
+ WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+ .path("/realms")
+ .path(TEST)
+ .path("protocol/openid-connect/token");
+ System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret", "user", "password");
+ String accessToken = tokenResponse.getAccessToken();
+
+ try (Response response = exchangeUrl.request()
+ .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "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")
+
+ ))) {
+ org.junit.Assert.assertEquals(200, response.getStatus());
+ AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+ String exchangedTokenString = accessTokenResponse.getToken();
+ JsonNode json = JsonSerialization.readValue(oauth.introspectAccessTokenWithClientCredential("client-exchanger", "secret", exchangedTokenString), com.fasterxml.jackson.databind.JsonNode.class);
+ assertTrue(json.get("active").asBoolean());
+ assertEquals("impersonated-user", json.get("preferred_username").asText());
+ assertEquals("user", json.get("act").get("sub").asText());
+ }
+
+ try (Response response = exchangeUrl.request()
+ .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "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")
+
+ ))) {
+ org.junit.Assert.assertEquals(200, response.getStatus());
+ AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+ String exchangedTokenString = accessTokenResponse.getToken();
+ JsonNode json = JsonSerialization.readValue(oauth.introspectAccessTokenWithClientCredential("client-exchanger", "secret", exchangedTokenString), com.fasterxml.jackson.databind.JsonNode.class);
+ assertTrue(json.get("active").asBoolean());
+ assertEquals("impersonated-user", json.get("preferred_username").asText());
+ assertEquals("user", json.get("act").get("sub").asText());
+ }
+ }
+
@UncaughtServerErrorExpected
@Test
public void testImpersonationUsingPublicClient() throws Exception {
@@ -512,7 +576,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
@@ -561,7 +625,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms")
@@ -621,7 +685,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "bad-impersonator");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
// test that user does not have impersonator permission
{
@@ -699,7 +763,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
Assert.assertEquals("direct-legal", exchangedToken.getIssuedFor());
Assert.assertEquals("target", exchangedToken.getAudience()[0]);
Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
- Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+ assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
}
// direct-public fails impersonation
@@ -728,7 +792,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
.param(OAuth2Constants.AUDIENCE, "target")
));
- Assert.assertTrue(response.getStatus() >= 400);
+ assertTrue(response.getStatus() >= 400);
response.close();
}
}
@@ -779,7 +843,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
response = oauth.doTokenExchange(TEST, accessToken, null, "client-exchanger", "secret");
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
@@ -799,7 +863,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
response = oauth.doTokenExchange(TEST, accessToken, "target", "direct-legal", "secret");
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
@@ -816,7 +880,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
AccessToken token = accessTokenVerifier.parse().getToken();
Assert.assertEquals(token.getPreferredUsername(), "user");
- Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+ assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
// public client has no permission to exchange with the client direct-legal to which the token was issued for
// if not set, the audience is calculated based on the client to which the token was issued for