Skip to content

Commit acb7abc

Browse files
rmartincmposolda
authored andcommitted
Make token exchange grant type supported by OIDC client registration
Closes #37554 Signed-off-by: rmartinc <[email protected]>
1 parent c5b391d commit acb7abc

File tree

2 files changed

+51
-13
lines changed

2 files changed

+51
-13
lines changed

services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.keycloak.models.utils.KeycloakModelUtils;
3434
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
3535
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
36+
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
3637
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
3738
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
3839
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
@@ -120,6 +121,7 @@ public static ClientRepresentation toInternal(KeycloakSession session, OIDCClien
120121
client.setServiceAccountsEnabled(oidcGrantTypes.contains(OAuth2Constants.CLIENT_CREDENTIALS));
121122
setOidcGrantEnabled(client, CibaConfig.OIDC_CIBA_GRANT_ENABLED, oidcGrantTypes.contains(OAuth2Constants.CIBA_GRANT_TYPE));
122123
setOidcGrantEnabled(client, OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, oidcGrantTypes.contains(OAuth2Constants.DEVICE_CODE_GRANT_TYPE));
124+
setOidcGrantEnabled(client, OIDCConfigAttributes.STANDARD_TOKEN_EXCHANGE_ENABLED, oidcGrantTypes.contains(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
123125
client.setAuthorizationServicesEnabled(oidcGrantTypes.contains(OAuth2Constants.UMA_GRANT_TYPE));
124126
configWrapper.setUseRefreshToken(oidcGrantTypes.contains(OAuth2Constants.REFRESH_TOKEN));
125127
}
@@ -132,6 +134,9 @@ public static ClientRepresentation toInternal(KeycloakSession session, OIDCClien
132134
if ("none".equals(authMethod)) {
133135
client.setClientAuthenticatorType("none");
134136
client.setPublicClient(Boolean.TRUE);
137+
if (oidcGrantTypes != null && oidcGrantTypes.contains(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)) {
138+
throw new ClientRegistrationException("Token Exchange cannot be enabled in a public client");
139+
}
135140
} else {
136141
ClientAuthenticatorFactory clientAuthFactory;
137142
if (authMethod == null) {
@@ -532,9 +537,13 @@ private static List<String> getOIDCGrantTypes(ClientRepresentation client) {
532537
if (client.getAuthorizationServicesEnabled() != null && client.getAuthorizationServicesEnabled()) {
533538
grantTypes.add(OAuth2Constants.UMA_GRANT_TYPE);
534539
}
535-
if (OIDCAdvancedConfigWrapper.fromClientRepresentation(client).isUseRefreshToken()) {
540+
OIDCAdvancedConfigWrapper oidcClient = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
541+
if (oidcClient.isUseRefreshToken()) {
536542
grantTypes.add(OAuth2Constants.REFRESH_TOKEN);
537543
}
544+
if (!client.isPublicClient() && oidcClient.isStandardTokenExchangeEnabled()) {
545+
grantTypes.add(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
546+
}
538547
return grantTypes;
539548
}
540549

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationResponseTypesAndGrantsTest.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@
1919

2020
package org.keycloak.testsuite.client;
2121

22+
import jakarta.ws.rs.core.Response;
2223
import java.util.Collections;
2324
import java.util.List;
2425

2526
import org.hamcrest.MatcherAssert;
27+
import org.hamcrest.Matchers;
2628
import org.junit.Before;
2729
import org.junit.Test;
30+
import org.keycloak.OAuthErrorException;
2831
import org.keycloak.client.registration.Auth;
2932
import org.keycloak.client.registration.ClientRegistrationException;
33+
import org.keycloak.client.registration.HttpErrorException;
3034
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
3135
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
3236
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
3337
import org.keycloak.representations.idm.ClientRepresentation;
38+
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
3439
import org.keycloak.representations.oidc.OIDCClientRepresentation;
40+
import org.keycloak.util.JsonSerialization;
3541
import org.keycloak.testsuite.Assert;
3642

3743
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -41,6 +47,7 @@
4147
import static org.keycloak.OAuth2Constants.IMPLICIT;
4248
import static org.keycloak.OAuth2Constants.PASSWORD;
4349
import static org.keycloak.OAuth2Constants.REFRESH_TOKEN;
50+
import static org.keycloak.OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE;
4451
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
4552
import static org.keycloak.protocol.oidc.utils.OIDCResponseType.CODE;
4653
import static org.keycloak.protocol.oidc.utils.OIDCResponseType.ID_TOKEN;
@@ -81,7 +88,7 @@ public void testClientWithoutResponseTypesAndGrantTypes() throws Exception {
8188

8289
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN));
8390

84-
assertKeycloakClient(response, true, false, false, false, true, false);
91+
assertKeycloakClient(response, true, false, false, false, true, false, false);
8592
}
8693

8794
@Test
@@ -92,7 +99,7 @@ public void testResponseTypeCodeWithoutGrantTypes() throws Exception {
9299

93100
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN));
94101

95-
assertKeycloakClient(response, true, false, false, false, true, false);
102+
assertKeycloakClient(response, true, false, false, false, true, false, false);
96103
}
97104

98105
// Limitation of Keycloak switches (Standard flow, Implicit flow) means that enabling any hybrid grant type enables also implicit flow.
@@ -106,7 +113,7 @@ public void testResponseTypeCodeIDTokenWithoutGrantTypes() throws Exception {
106113
assertOIDCResponse(response, List.of(CODE, NONE, ID_TOKEN, "id_token token", "code id_token", "code token", "code id_token token"),
107114
List.of(AUTHORIZATION_CODE, IMPLICIT, REFRESH_TOKEN));
108115

109-
assertKeycloakClient(response, true, true, false, false, true, false);
116+
assertKeycloakClient(response, true, true, false, false, true, false, false);
110117
}
111118

112119
@Test
@@ -118,7 +125,7 @@ public void testWithoutResponseTypeClientCredentialsGrant() throws Exception {
118125
assertOIDCResponse(response, Collections.emptyList(),
119126
List.of(CLIENT_CREDENTIALS));
120127

121-
assertKeycloakClient(response, false, false, false, true, false, false);
128+
assertKeycloakClient(response, false, false, false, true, false, false, false);
122129
}
123130

124131
@Test
@@ -130,7 +137,7 @@ public void testWithoutResponseTypePasswordGrant() throws Exception {
130137
assertOIDCResponse(response, Collections.emptyList(),
131138
List.of(PASSWORD));
132139

133-
assertKeycloakClient(response, false, false, true, false, false, false);
140+
assertKeycloakClient(response, false, false, true, false, false, false, false);
134141
}
135142

136143
@Test
@@ -142,7 +149,7 @@ public void testWithoutResponseTypePasswordClientCredentialsRefreshTokensGrants(
142149
assertOIDCResponse(response, Collections.emptyList(),
143150
List.of(PASSWORD, CLIENT_CREDENTIALS, REFRESH_TOKEN));
144151

145-
assertKeycloakClient(response, false, false, true, true, true, false);
152+
assertKeycloakClient(response, false, false, true, true, true, false, false);
146153
}
147154

148155
@Test
@@ -153,7 +160,7 @@ public void testClientWithoutRefreshToken() throws Exception {
153160

154161
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE));
155162

156-
assertKeycloakClient(response, true, false, false, false, false, false);
163+
assertKeycloakClient(response, true, false, false, false, false, false, false);
157164
}
158165

159166
@Test
@@ -164,7 +171,7 @@ public void testClientWithRefreshToken() throws Exception {
164171

165172
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN));
166173

167-
assertKeycloakClient(response, true, false, false, false, true, false);
174+
assertKeycloakClient(response, true, false, false, false, true, false, false);
168175
}
169176

170177
@Test
@@ -175,7 +182,27 @@ public void testNoResponseTypesWithDeviceGrant() throws Exception {
175182

176183
assertOIDCResponse(response, Collections.emptyList(), List.of(DEVICE_CODE_GRANT_TYPE));
177184

178-
assertKeycloakClient(response, false, false, false, false, false, true);
185+
assertKeycloakClient(response, false, false, false, false, false, true, false);
186+
}
187+
188+
@Test
189+
public void testGrantTypeTokenExchange() throws Exception {
190+
OIDCClientRepresentation clientRep = createRep(null, List.of(AUTHORIZATION_CODE, REFRESH_TOKEN, TOKEN_EXCHANGE_GRANT_TYPE));
191+
clientRep.setTokenEndpointAuthMethod("none");
192+
193+
ClientRegistrationException clientRegistrationException = Assert.assertThrows(ClientRegistrationException.class, () -> reg.oidc().create(clientRep));
194+
MatcherAssert.assertThat(clientRegistrationException.getCause(), Matchers.instanceOf(HttpErrorException.class));
195+
HttpErrorException httpErrorException = (HttpErrorException) clientRegistrationException.getCause();
196+
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), httpErrorException.getStatusLine().getStatusCode());
197+
OAuth2ErrorRepresentation error = JsonSerialization.readValue(httpErrorException.getErrorResponse(), OAuth2ErrorRepresentation.class);
198+
Assert.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, error.getError());
199+
200+
clientRep.setTokenEndpointAuthMethod(null);
201+
OIDCClientRepresentation response = reg.oidc().create(clientRep);
202+
203+
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN, TOKEN_EXCHANGE_GRANT_TYPE));
204+
205+
assertKeycloakClient(response, true, false, false, false, true, false, true);
179206
}
180207

181208
@Test
@@ -186,7 +213,7 @@ public void testCodeResponseTypeWithMoreGrants() throws Exception {
186213

187214
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS));
188215

189-
assertKeycloakClient(response, true, false, true, true, true, false);
216+
assertKeycloakClient(response, true, false, true, true, true, false, false);
190217
}
191218

192219
// Grant type "authorization_code" added automatically because of response_type "code" .
@@ -201,7 +228,7 @@ public void testCodeResponseTypeWithIncompatibleGrants() throws Exception {
201228

202229
assertOIDCResponse(response, List.of(CODE, NONE), List.of(AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS));
203230

204-
assertKeycloakClient(response, true, false, true, true, true, false);
231+
assertKeycloakClient(response, true, false, true, true, true, false, false);
205232
}
206233

207234
private void assertOIDCResponse(OIDCClientRepresentation response, List<String> expectedResponseTypes, List<String> expectedGrantTypes) {
@@ -228,7 +255,8 @@ private void assertKeycloakClient(OIDCClientRepresentation response,
228255
boolean expectedDirectGrantFlow,
229256
boolean expectedServiceAccountsFlow,
230257
boolean expectedRefreshToken,
231-
boolean expectedDeviceGrant) {
258+
boolean expectedDeviceGrant,
259+
boolean expectedTokenExchange) {
232260
ClientRepresentation kcClient = getClient(response.getClientId());
233261
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
234262
Assert.assertEquals("Expected standard flow: " + expectedStandardFlow + " did not match.", expectedStandardFlow, kcClient.isStandardFlowEnabled());
@@ -239,5 +267,6 @@ private void assertKeycloakClient(OIDCClientRepresentation response,
239267
Assert.assertFalse("Don't expect refresh token for client credentials grant enabled", config.isUseRefreshTokenForClientCredentialsGrant());
240268
boolean deviceEnabled = kcClient.getAttributes() != null && Boolean.parseBoolean(kcClient.getAttributes().get(OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED));
241269
Assert.assertEquals("Expected device: " + expectedDeviceGrant + " did not match.", expectedDeviceGrant, deviceEnabled);
270+
Assert.assertEquals("Expected Token Exchange: " + expectedTokenExchange + " did not match.", expectedTokenExchange, config.isStandardTokenExchangeEnabled());
242271
}
243272
}

0 commit comments

Comments
 (0)