From 3631a413d28bbcdaeadc8bed8a6cd47a478c9a3c Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 28 Jun 2022 11:28:48 -0300 Subject: [PATCH] Allow token exchange when subjec_token is not associated with a session Closes #12596 --- .../oidc/DefaultTokenExchangeProvider.java | 8 +++++ .../oauth/ClientTokenExchangeTest.java | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) 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 9c16f715e7..d2e68d1607 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java @@ -29,6 +29,7 @@ import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate; import org.keycloak.common.ClientConnection; +import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.Base64Url; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -370,6 +371,13 @@ public class DefaultTokenExchangeProvider implements TokenExchangeProvider { authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); + if (targetUserSession == null) { + // if no session is associated with a subject_token, a stateless session is created to only allow building a token to the audience + targetUserSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, targetUser, targetUser.getUsername(), + clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); + + } + event.session(targetUserSession); AuthenticationManager.setClientScopesInSession(authSession); 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 65b7f6540a..25d343fd95 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 @@ -209,6 +209,15 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest { noRefreshToken.setFullScopeAllowed(false); noRefreshToken.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false"); + ClientModel serviceAccount = realm.addClient("my-service-account"); + serviceAccount.setClientId("my-service-account"); + serviceAccount.setPublicClient(false); + serviceAccount.setServiceAccountsEnabled(true); + serviceAccount.setEnabled(true); + serviceAccount.setSecret("secret"); + serviceAccount.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + serviceAccount.setFullScopeAllowed(false); + // permission for client to client exchange to "target" client ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); clientRep.setName("to"); @@ -216,6 +225,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest { clientRep.addClient(legal.getId()); clientRep.addClient(directLegal.getId()); clientRep.addClient(noRefreshToken.getId()); + clientRep.addClient(serviceAccount.getId()); ResourceServer server = management.realmResourceServer(); Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep); @@ -305,6 +315,27 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest { } } + @Test + @UncaughtServerErrorExpected + public void testExchangeUsingServiceAccount() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("my-service-account"); + OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret"); + String accessToken = response.getAccessToken(); + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "my-service-account", "secret"); + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals("my-service-account", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "service-account-my-service-account"); + } + } + @Test @UncaughtServerErrorExpected public void testExchangeFromPublicClient() throws Exception {