Enable configurable client_id parameter validation for federated client assertions (#48026)

Closes #48024


Signed-off-by: Sebastian Łaskawiec <sebastian.laskawiec@defenseunicorns.com>
This commit is contained in:
Sebastian Łaskawiec
2026-05-26 15:14:36 +02:00
committed by GitHub
parent 71e63e99dc
commit 3e8a1310d9
3 changed files with 36 additions and 9 deletions
@@ -101,12 +101,6 @@ public abstract class AbstractJWTClientValidator extends AbstractBaseJWTValidato
return failure("Token sub claim is required");
}
String clientIdParam = context.getHttpRequest().getDecodedFormParameters().getFirst(OAuth2Constants.CLIENT_ID);
if (clientIdParam != null && !clientIdParam.equals(clientId)) {
logger.debug("client_id parameter does not match JWT subject");
return failure("client_id parameter does not match sub claim");
}
String expectedTokenIssuer = getExpectedTokenIssuer();
if (expectedTokenIssuer != null && !expectedTokenIssuer.equals(token.getIssuer())) {
return false;
@@ -116,11 +110,16 @@ public abstract class AbstractJWTClientValidator extends AbstractBaseJWTValidato
if (client == null) {
return failure(AuthenticationFlowError.CLIENT_NOT_FOUND);
} else {
context.getEvent().client(client.getClientId());
context.setClient(client);
}
String clientIdParam = context.getHttpRequest().getDecodedFormParameters().getFirst(OAuth2Constants.CLIENT_ID);
if (clientIdParam != null && !clientIdParam.equals(client.getClientId())) {
return failure("client_id parameter does not match authenticated client");
}
context.getEvent().client(client.getClientId());
context.setClient(client);
if (!client.isEnabled()) {
return failure(AuthenticationFlowError.CLIENT_DISABLED);
}
@@ -18,6 +18,7 @@ import org.keycloak.testframework.realm.IdentityProviderBuilder;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
@@ -72,6 +73,30 @@ public class KubernetesClientAuthTest extends AbstractBaseClientAuthTest {
assertSuccess(internalClientId, jwt.getId(), expectedTokenIssuer, externalClientId, events.poll());
}
@Test
public void testValidTokenWithClientId() {
JsonWebToken jwt = createDefaultToken();
String jws = getIdentityProvider().encodeToken(jwt);
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest()
.client(INTERNAL_CLIENT_ID)
.clientJwt(jws, getClientAssertionType())
.send();
assertSuccess(internalClientId, response);
assertSuccess(internalClientId, jwt.getId(), expectedTokenIssuer, externalClientId, events.poll());
}
@Test
public void testWrongClientIdFailsValidation() {
JsonWebToken jwt = createDefaultToken();
String jws = getIdentityProvider().encodeToken(jwt);
AccessTokenResponse response = oAuthClient.clientCredentialsGrantRequest()
.client("completely-wrong-id")
.clientJwt(jws, getClientAssertionType())
.send();
assertFailure(response);
assertFailure("completely-wrong-id", expectedTokenIssuer, externalClientId, jwt.getId(), "client_not_found", events.poll());
}
@Test
public void testReuse() {
JsonWebToken jwt = createDefaultToken();
@@ -148,6 +148,9 @@ public abstract class AbstractHttpPostRequest<T, R> {
if (clientAssertion != null && clientAssertionType != null) {
parameter("client_assertion_type", clientAssertionType);
parameter("client_assertion", clientAssertion);
if (this.clientId != null) {
parameter("client_id", this.clientId);
}
} else if (tokenType != null && tokenValue != null) {
header("Authorization", tokenType + " " + tokenValue);
} else if (clientSecret != null) {