Use correct error response for missing assertions in Signed JWT Validation

* Ensure conformance for Signed JWT Validation (#43269)

This re-adds the explicit client assertion parameter validation to produce the correct error responses required by RFC7523.
See: https://www.rfc-editor.org/rfc/rfc7523.html#section-3.2

The refactoring for the support for Federated JWT Client authentication broke the OIDF conformance tests for https://www.rfc-editor.org/rfc/rfc7523.html.

Fixes #43269
Fixes #43270

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>

* Ensure conformance for Signed JWT Validation (#43269)

Add additional tests for ClientAuthSignedJWTTest.

Fixes #43269

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>

---------

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
Thomas Darimont 2025-10-08 11:01:13 +02:00 committed by GitHub
parent 12ae8b7cc9
commit 85afd62452
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 0 deletions

View File

@ -1,6 +1,11 @@
package org.keycloak.authentication.authenticators.client;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.http.HttpRequest;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -38,6 +43,11 @@ public class JWTClientValidator extends AbstractJWTClientValidator {
return expectedAudiences;
}
@Override
public boolean validate() {
return clientAssertionParametersValidation() && super.validate();
}
@Override
protected boolean isMultipleAudienceAllowed() {
OIDCLoginProtocol loginProtocol = (OIDCLoginProtocol) context.getSession().getProvider(LoginProtocol.class, OIDCLoginProtocol.LOGIN_PROTOCOL);
@ -64,4 +74,44 @@ public class JWTClientValidator extends AbstractJWTClientValidator {
return OIDCAdvancedConfigWrapper.fromClientModel(client).getTokenEndpointAuthSigningAlg();
}
public boolean clientAssertionParametersValidation() {
//KEYCLOAK-19461: Needed for quarkus resteasy implementation throws exception when called with mediaType authentication/json in OpenShiftTokenReviewEndpoint
if(!isFormDataRequest(context.getHttpRequest())) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return false;
}
var params = context.getHttpRequest().getDecodedFormParameters();
String clientAssertionType = params.getFirst(OAuth2Constants.CLIENT_ASSERTION_TYPE);
var clientAssertion = params.getFirst(OAuth2Constants.CLIENT_ASSERTION);
if (clientAssertionType == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type is missing");
context.challenge(challengeResponse);
return false;
}
if (!clientAssertionType.equals(OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT)) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "Parameter client_assertion_type has value '"
+ clientAssertionType + "' but expected is '" + OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT + "'");
context.challenge(challengeResponse);
return false;
}
if (clientAssertion == null) {
Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_client", "client_assertion parameter missing");
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
return false;
}
return true;
}
private boolean isFormDataRequest(HttpRequest request) {
MediaType mediaType = request.getHttpHeaders().getMediaType();
return mediaType != null && mediaType.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
}
}

View File

@ -442,6 +442,32 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
}
@Test
public void testWithClientAndMissingClientAssertionType() throws Exception {
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "client1"));
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
CloseableHttpResponse resp = sendRequest(oauth.getEndpoints().getToken(), parameters);
AccessTokenResponse response = new AccessTokenResponse(resp);
assertError(response, 400, "client1", "invalid_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
public void testWithClientAndInvalidClientAssertionType() throws Exception {
List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "client1"));
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ASSERTION_TYPE, "invalid"));
CloseableHttpResponse resp = sendRequest(oauth.getEndpoints().getToken(), parameters);
AccessTokenResponse response = new AccessTokenResponse(resp);
assertError(response,400, "client1", "invalid_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
public void testMissingClientAssertion() throws Exception {
List<NameValuePair> parameters = new LinkedList<NameValuePair>();