From 282260dc9577eb50b9f8ea2b50e109ba2ac730a0 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 29 Jul 2024 15:12:29 +0200 Subject: [PATCH] Ensure issued_client_type is always added to successful token-exchange response (#31548) - Compute issued_token_type response parameter based on requested_token_type and client configuration - `issued_token_type` is a required response parameter as per [RFC8693 2.2.1](https://datatracker.ietf.org/doc/html/rfc8693#section-2.2.1) - Added test to ClientTokenExchangeTest that requests an access-token as requested-token-type Fixes #31548 Signed-off-by: Thomas Darimont --- .../oidc/DefaultTokenExchangeProvider.java | 6 ++++ .../oauth/ClientTokenExchangeTest.java | 32 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) 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 03d9ca4edf29..7bfb925bf88e 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java @@ -437,11 +437,15 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM responseBuilder.getAccessToken().setSessionId(null); } + String issuedTokenType; if (requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE) && OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRefreshToken() && targetUserSession.getPersistenceState() != UserSessionModel.SessionPersistenceState.TRANSIENT) { responseBuilder.generateRefreshToken(); responseBuilder.getRefreshToken().issuedFor(client.getClientId()); + issuedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE; + } else { + issuedTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE; } String scopeParam = clientSessionCtx.getClientSession().getNote(OAuth2Constants.SCOPE); @@ -450,6 +454,8 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM } AccessTokenResponse res = responseBuilder.build(); + res.setOtherClaims(OAuth2Constants.ISSUED_TOKEN_TYPE, issuedTokenType); + event.detail(Details.AUDIENCE, targetClient.getClientId()) .user(targetUser); 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 2c423f5a2df4..67bc8c88ecd9 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 @@ -304,6 +304,7 @@ public void testExchange() throws Exception { { response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret"); + Assert.assertEquals(OAuth2Constants.REFRESH_TOKEN_TYPE, response.getIssuedTokenType()); String exchangedTokenString = response.getAccessToken(); TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); AccessToken exchangedToken = verifier.parse().getToken(); @@ -316,7 +317,7 @@ public void testExchange() throws Exception { { response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret"); - + Assert.assertEquals(OAuth2Constants.REFRESH_TOKEN_TYPE, response.getIssuedTokenType()); String exchangedTokenString = response.getAccessToken(); TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); AccessToken exchangedToken = verifier.parse().getToken(); @@ -332,6 +333,34 @@ public void testExchange() throws Exception { } } + @Test + public void testExchangeRequestAccessTokenType() throws Exception { + testingClient.server().run(ClientTokenExchangeTest::setupRealm); + + oauth.realm(TEST); + oauth.clientId("client-exchanger"); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password"); + String accessToken = response.getAccessToken(); + TokenVerifier accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class); + AccessToken token = accessTokenVerifier.parse().getToken(); + Assert.assertNotNull(token.getSessionId()); + Assert.assertEquals(token.getPreferredUsername(), "user"); + assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example")); + + { + response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret", Map.of(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)); + Assert.assertEquals(OAuth2Constants.ACCESS_TOKEN_TYPE, response.getIssuedTokenType()); + String exchangedTokenString = response.getAccessToken(); + TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); + AccessToken exchangedToken = verifier.parse().getToken(); + Assert.assertEquals(token.getSessionId(), exchangedToken.getSessionId()); + Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor()); + Assert.assertEquals("target", exchangedToken.getAudience()[0]); + Assert.assertEquals(exchangedToken.getPreferredUsername(), "user"); + assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); + } + } + @Test @UncaughtServerErrorExpected public void testExchangeUsingServiceAccount() throws Exception { @@ -347,6 +376,7 @@ public void testExchangeUsingServiceAccount() throws Exception { { response = oauth.doTokenExchange(TEST, accessToken, "target", "my-service-account", "secret"); + Assert.assertEquals(OAuth2Constants.ACCESS_TOKEN_TYPE, response.getIssuedTokenType()); String exchangedTokenString = response.getAccessToken(); TokenVerifier verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class); AccessToken exchangedToken = verifier.parse().getToken();