From 60bf57cd13f40be68a033ac7355e6d7ee7450777 Mon Sep 17 00:00:00 2001 From: Marek Posolda Date: Mon, 13 Jan 2025 17:36:44 +0100 Subject: [PATCH] Failed to authenticate client with method client_secret_jwt when client has keys generated closes #34547 Signed-off-by: mposolda (cherry picked from commit 9b01e958dc73ab7634c4e49b5730cd9cd579129a) --- .../client/JWTClientAuthenticator.java | 17 ++++++++- .../client/JWTClientSecretAuthenticator.java | 17 ++++++++- .../client/JWTClientValidator.java | 9 ++++- .../oauth/ClientAuthSecretSignedJWTTest.java | 37 +++++++++++++++++++ .../oauth/ClientAuthSignedJWTTest.java | 23 ++++++++++++ 5 files changed, 98 insertions(+), 5 deletions(-) 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");