mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
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:
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user