Failed to authenticate client with method client_secret_jwt when client has keys generated

closes #34547

Signed-off-by: mposolda <mposolda@gmail.com>
(cherry picked from commit 9b01e958dc)
This commit is contained in:
Marek Posolda
2025-01-13 17:36:44 +01:00
committed by GitHub
parent 772b1fdaad
commit 60bf57cd13
5 changed files with 98 additions and 5 deletions

View File

@@ -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 <a href="https://tools.ietf.org/html/rfc7519">specs</a> 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;

View File

@@ -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 <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> 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()) {

View File

@@ -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<String, String> 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;
}

View File

@@ -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");

View File

@@ -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");