Skip to content

Commit

Permalink
Ensure issued_client_type is always added to successful token-exchang…
Browse files Browse the repository at this point in the history
…e response (keycloak#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 keycloak#31548

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
  • Loading branch information
thomasdarimont authored and mposolda committed Jul 30, 2024
1 parent a6c70d6 commit 282260d
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Expand All @@ -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<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Expand All @@ -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<AccessToken> 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<AccessToken> 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 {
Expand All @@ -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<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
AccessToken exchangedToken = verifier.parse().getToken();
Expand Down

0 comments on commit 282260d

Please sign in to comment.