diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
index 7382897db4c..90b8cd8a618 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java
@@ -34,6 +34,7 @@ import jakarta.ws.rs.core.Response;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.AuthenticationExecutionModel;
@@ -49,6 +50,8 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
+import static org.keycloak.models.TokenManager.DEFAULT_VALIDATOR;
+
/**
* Client authentication based on JWT signed by client private key .
* See specs for more details.
@@ -67,7 +70,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
- JWTClientValidator validator = new JWTClientValidator(context);
+ JWTClientValidator validator = new JWTClientValidator(context, getId());
if (!validator.clientAssertionParametersValidation()) return;
try {
@@ -90,7 +93,17 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
boolean signatureValid;
try {
- JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
+ JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, (jose, validatedClient) -> {
+ DEFAULT_VALIDATOR.accept(jose, validatedClient);
+ String signatureAlgorithm = jose.getHeader().getRawAlgorithm();
+ ClientSignatureVerifierProvider signatureProvider = context.getSession().getProvider(ClientSignatureVerifierProvider.class, signatureAlgorithm);
+ if (signatureProvider == null) {
+ throw new RuntimeException("Algorithm not supported");
+ }
+ if (!signatureProvider.isAsymmetricAlgorithm()) {
+ throw new RuntimeException("Algorithm is not asymmetric");
+ }
+ }, JsonWebToken.class);
signatureValid = jwt != null;
} catch (RuntimeException e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java
index 9e967e17070..07068844057 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientSecretAuthenticator.java
@@ -18,6 +18,7 @@ package org.keycloak.authentication.authenticators.client;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
+import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.ClientModel;
@@ -41,6 +42,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import static org.keycloak.models.TokenManager.DEFAULT_VALIDATOR;
+
/**
* Client authentication based on JWT signed by client secret instead of private key .
* See specs for more details.
@@ -55,7 +58,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
- JWTClientValidator validator = new JWTClientValidator(context);
+ JWTClientValidator validator = new JWTClientValidator(context, getId());
if (!validator.clientAssertionParametersValidation()) return;
try {
@@ -85,7 +88,17 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
boolean signatureValid;
try {
- JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, JsonWebToken.class);
+ JsonWebToken jwt = context.getSession().tokens().decodeClientJWT(clientAssertion, client, (jose, validatedClient) -> {
+ DEFAULT_VALIDATOR.accept(jose, validatedClient);
+ String signatureAlgorithm = jose.getHeader().getRawAlgorithm();
+ ClientSignatureVerifierProvider signatureProvider = context.getSession().getProvider(ClientSignatureVerifierProvider.class, signatureAlgorithm);
+ if (signatureProvider == null) {
+ throw new RuntimeException("Algorithm not supported");
+ }
+ if (signatureProvider.isAsymmetricAlgorithm()) {
+ throw new RuntimeException("Algorithm is not symmetric");
+ }
+ }, JsonWebToken.class);
signatureValid = jwt != null;
//try authenticate with client rotated secret
if (!signatureValid && wrapper.hasRotatedSecret() && !wrapper.isClientRotatedSecretExpired()) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
index 8c4d5487f75..f9f21633e5a 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
@@ -48,6 +48,7 @@ public class JWTClientValidator {
private final ClientAuthenticationFlowContext context;
private final RealmModel realm;
private final int currentTime;
+ private final String clientAuthenticatorProviderId;
private MultivaluedMap params;
private String clientAssertion;
@@ -55,10 +56,11 @@ public class JWTClientValidator {
private JsonWebToken token;
private ClientModel client;
- public JWTClientValidator(ClientAuthenticationFlowContext context) {
+ public JWTClientValidator(ClientAuthenticationFlowContext context, String clientAuthenticatorProviderId) {
this.context = context;
this.realm = context.getRealm();
this.currentTime = Time.currentTime();
+ this.clientAuthenticatorProviderId = clientAuthenticatorProviderId;
}
public boolean clientAssertionParametersValidation() {
@@ -134,6 +136,11 @@ public class JWTClientValidator {
return false;
}
+ if (!clientAuthenticatorProviderId.equals(client.getClientAuthenticatorType())) {
+ context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, null);
+ return false;
+ }
+
return true;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java
index ff43586b868..5e4e2a9bd34 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSecretSignedJWTTest.java
@@ -42,7 +42,9 @@ import org.jetbrains.annotations.NotNull;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder;
@@ -128,6 +130,41 @@ public class ClientAuthSecretSignedJWTTest extends AbstractKeycloakTest {
testCodeToTokenRequestSuccess(Algorithm.HS512);
}
+
+ // Issue 34547
+ @Test
+ public void testCodeToTokenRequestSuccessWhenClientHasGeneratedKeys() throws Exception {
+ // Test when client has public/private keys generated despite the fact that it uses client-secret for the client authentication (and not those keys)
+ ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app").getCertficateResource("jwt.credential").generate();
+
+ testCodeToTokenRequestSuccess(Algorithm.HS256);
+ }
+
+ @Test
+ public void testCodeToTokenRequestFailureWhenClientHasPrivateKeyJWT() throws Exception {
+ // Setup client for "private_key_jwt" authentication
+ ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+ client.getCertficateResource("jwt.credential").generate();
+ ClientRepresentation clientRep = client.toRepresentation();
+ clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
+ client.update(clientRep);
+
+ // Client should not be able to authenticate with "client_secret_jwt"
+ try {
+ oauth.clientId("test-app");
+ oauth.doLogin("test-user@localhost", "password");
+ events.expectLogin().client("test-app").assertEvent();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse response = doAccessTokenRequest(code, getClientSignedJWT(CLIENT_SECRET, 20, Algorithm.HS256));
+ assertEquals(400, response.getStatusCode());
+ assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
+ } finally {
+ clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
+ client.update(clientRep);
+ }
+ }
+
@Test
public void testInvalidIssuer() throws Exception {
oauth.clientId("test-app");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index b48ce003aa3..5949855ae75 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -27,6 +27,7 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
+import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.crypto.Algorithm;
@@ -671,6 +672,28 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
assertEquals(OAuthErrorException.INVALID_CLIENT, response.getError());
}
+ @Test
+ public void testAuthenticationFailsWhenClientSecretJWTAuthenticatorSet() throws Exception {
+ // Set client authenticator to JWT signed by client secret.
+ ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), "client1");
+ ClientRepresentation clientRep = clientResource.toRepresentation();
+ clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID);
+ clientResource.update(clientRep);
+
+ // It should not be possible to use private_key_jwt for the authentication
+ try {
+ String clientJwt = getClient1SignedJWT();
+
+ OAuthClient.AccessTokenResponse response = doClientCredentialsGrantRequest(clientJwt);
+
+ assertEquals(400, response.getStatusCode());
+ assertEquals(OAuthErrorException.UNAUTHORIZED_CLIENT, response.getError());
+ } finally {
+ clientRep.setClientAuthenticatorType(JWTClientAuthenticator.PROVIDER_ID);
+ clientResource.update(clientRep);
+ }
+ }
+
@Test
public void testMissingIdClaim() throws Exception {
OAuthClient.AccessTokenResponse response = testMissingClaim("id");