Skip to content

Commit 8923973

Browse files
graziangmposolda
authored andcommitted
Remove public client support for standard token exchange v2
Closes #37111 Signed-off-by: Giuseppe Graziano <[email protected]>
1 parent 8cd97dd commit 8923973

File tree

4 files changed

+63
-23
lines changed

4 files changed

+63
-23
lines changed

services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/AbstractTokenExchangeProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ protected void validateAudience(AccessToken token, boolean disallowOnHolderOfTok
267267
forbiddenIfClientIsNotTokenHolder(disallowOnHolderOfTokenMismatch, tokenHolder);
268268
} else if (!client.equals(tokenHolder)) {
269269
// confidential clients can only exchange to themselves if they are within the token audience
270-
forbiddenIfClientIsNotWithinTokenAudience(token, tokenHolder);
270+
forbiddenIfClientIsNotWithinTokenAudience(token);
271271
}
272272
} else {
273273
if (client.isPublicClient()) {
@@ -308,7 +308,7 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel
308308
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
309309
}
310310

311-
protected void forbiddenIfClientIsNotWithinTokenAudience(AccessToken token, ClientModel tokenHolder) {
311+
protected void forbiddenIfClientIsNotWithinTokenAudience(AccessToken token) {
312312
if (token != null && !token.hasAudience(client.getClientId())) {
313313
event.detail(Details.REASON, "client is not within the token audience");
314314
event.error(Errors.NOT_ALLOWED);

services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -128,26 +128,17 @@ protected Response tokenExchange() {
128128

129129
protected void validateAudience(AccessToken token, boolean disallowOnHolderOfTokenMismatch, List<ClientModel> targetAudienceClients) {
130130
ClientModel tokenHolder = token == null ? null : realm.getClientByClientId(token.getIssuedFor());
131+
132+
if (client.isPublicClient()) {
133+
String errorMessage = "Public client is not allowed to exchange token";
134+
event.detail(Details.REASON, errorMessage);
135+
event.error(Errors.INVALID_CLIENT);
136+
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, errorMessage, Response.Status.BAD_REQUEST);
137+
}
138+
131139
//reject if the requester-client is not in the audience of the subject token
132140
if (!client.equals(tokenHolder)) {
133-
forbiddenIfClientIsNotWithinTokenAudience(token, null);
134-
}
135-
for (ClientModel targetClient : targetAudienceClients) {
136-
boolean isClientTheAudience = targetClient.equals(client);
137-
if (isClientTheAudience) {
138-
if (client.isPublicClient()) {
139-
// public clients can only exchange on to themselves if they are the token holder
140-
forbiddenIfClientIsNotTokenHolder(disallowOnHolderOfTokenMismatch, tokenHolder);
141-
} else if (!client.equals(tokenHolder)) {
142-
// confidential clients can only exchange to themselves if they are within the token audience
143-
forbiddenIfClientIsNotWithinTokenAudience(token, tokenHolder);
144-
}
145-
} else {
146-
if (client.isPublicClient()) {
147-
// public clients can not exchange tokens from other client
148-
forbiddenIfClientIsNotTokenHolder(disallowOnHolderOfTokenMismatch, tokenHolder);
149-
}
150-
}
141+
forbiddenIfClientIsNotWithinTokenAudience(token);
151142
}
152143
}
153144

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV2Test.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ public void testClientExchangeToItselfWithConsents() throws Exception {
207207
client.update(clientRepresentation);
208208
}
209209

210+
@Test
211+
public void testExchangeWithPublicClient() throws Exception {
212+
String accessToken = resourceOwnerLogin("john", "password","subject-client", "secret");
213+
AccessTokenResponse response = tokenExchange(accessToken, "requester-client-public", null, null, null);
214+
org.junit.Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
215+
org.junit.Assert.assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
216+
org.junit.Assert.assertEquals("Public client is not allowed to exchange token", response.getErrorDescription());
217+
}
218+
210219
@Test
211220
public void testOptionalScopeParamRequestedWithoutAudience() throws Exception {
212221
String accessToken = resourceOwnerLogin("john", "password","subject-client", "secret");

testsuite/integration-arquillian/tests/base/src/test/resources/token-exchange/testrealm-token-exchange-v2.json

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,8 @@
261261
"containerId" : "a11ebbaf-c0fd-46df-9fe7-64e94ac8d945",
262262
"attributes" : { }
263263
} ],
264-
"invalid-requester-client" : [ ],
265264
"requester-client" : [ ],
266265
"security-admin-console" : [ ],
267-
"subject-client" : [ ],
268-
"admin-cli" : [ ],
269266
"target-client1" : [ {
270267
"id" : "b7eb747d-7aac-4e36-8ade-5b9875c9ed07",
271268
"name" : "target-client1-role",
@@ -294,7 +291,11 @@
294291
"containerId" : "52fbbee7-44ca-4a36-814f-95f18f632994",
295292
"attributes" : { }
296293
} ],
294+
"requester-client-public" : [ ],
297295
"target-client3" : [ ],
296+
"invalid-requester-client" : [ ],
297+
"subject-client" : [ ],
298+
"admin-cli" : [ ],
298299
"account" : [ {
299300
"id" : "4d92df58-f573-4dcc-9428-83c67595b0d3",
300301
"name" : "manage-consent",
@@ -1252,6 +1253,44 @@
12521253
"nodeReRegistrationTimeout" : -1,
12531254
"defaultClientScopes" : [ "service_account", "acr", "default-scope1", "roles", "basic" ],
12541255
"optionalClientScopes" : [ "optional-scope2" ]
1256+
}, {
1257+
"id" : "2daeae03-ff78-4f79-8e72-1c4d443e1655",
1258+
"clientId" : "requester-client-public",
1259+
"name" : "",
1260+
"description" : "",
1261+
"rootUrl" : "",
1262+
"adminUrl" : "",
1263+
"baseUrl" : "",
1264+
"surrogateAuthRequired" : false,
1265+
"enabled" : true,
1266+
"alwaysDisplayInConsole" : false,
1267+
"clientAuthenticatorType" : "client-secret",
1268+
"redirectUris" : [ "/*" ],
1269+
"webOrigins" : [ "/*" ],
1270+
"notBefore" : 0,
1271+
"bearerOnly" : false,
1272+
"consentRequired" : false,
1273+
"standardFlowEnabled" : true,
1274+
"implicitFlowEnabled" : false,
1275+
"directAccessGrantsEnabled" : true,
1276+
"serviceAccountsEnabled" : false,
1277+
"publicClient" : true,
1278+
"frontchannelLogout" : true,
1279+
"protocol" : "openid-connect",
1280+
"attributes" : {
1281+
"realm_client" : "false",
1282+
"oidc.ciba.grant.enabled" : "false",
1283+
"backchannel.logout.session.required" : "true",
1284+
"frontchannel.logout.session.required" : "true",
1285+
"oauth2.device.authorization.grant.enabled" : "false",
1286+
"display.on.consent.screen" : "false",
1287+
"backchannel.logout.revoke.offline.tokens" : "false"
1288+
},
1289+
"authenticationFlowBindingOverrides" : { },
1290+
"fullScopeAllowed" : true,
1291+
"nodeReRegistrationTimeout" : -1,
1292+
"defaultClientScopes" : [ "acr", "roles", "basic" ],
1293+
"optionalClientScopes" : [ ]
12551294
}, {
12561295
"id" : "9d94d530-3335-4bb9-bea8-e9476a812473",
12571296
"clientId" : "security-admin-console",
@@ -2151,6 +2190,7 @@
21512190
"xRobotsTag" : "none",
21522191
"xFrameOptions" : "SAMEORIGIN",
21532192
"contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
2193+
"xXSSProtection" : "1; mode=block",
21542194
"strictTransportSecurity" : "max-age=31536000; includeSubDomains"
21552195
},
21562196
"smtpServer" : { },

0 commit comments

Comments
 (0)