[OID4VCI-HAIP] Pass oid4vci-1_0-issuer-metadata_test

Signed-off-by: Thomas Diesler <tdiesler@proton.me>
This commit is contained in:
Thomas Diesler
2026-03-16 14:30:47 +01:00
committed by Marek Posolda
parent 29aeb894fc
commit 22e018cfdf
13 changed files with 248 additions and 16 deletions
@@ -98,6 +98,7 @@ public class Profile {
STEP_UP_AUTHENTICATION_SAML("Step-up Authentication Saml", Type.PREVIEW, Feature.STEP_UP_AUTHENTICATION),
CLIENT_AUTH_FEDERATED("Authenticates client based on assertions issued by identity provider", Type.DEFAULT),
CLIENT_AUTH_ABCA("Attestation-Based Client Authentication", Type.EXPERIMENTAL),
SPIFFE("SPIFFE trust relationship provider", Type.PREVIEW),
@@ -115,6 +115,12 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("token_endpoint_auth_signing_alg_values_supported")
private List<String> tokenEndpointAuthSigningAlgValuesSupported;
@JsonProperty("client_attestation_signing_alg_values_supported")
private List<String> clientAttestationSigningAlgValuesSupported;
@JsonProperty("client_attestation_pop_signing_alg_values_supported")
private List<String> clientAttestationPopSigningAlgValuesSupported;
@JsonProperty("introspection_endpoint_auth_methods_supported")
private List<String> introspectionEndpointAuthMethodsSupported;
@@ -410,6 +416,22 @@ public class OIDCConfigurationRepresentation {
this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported;
}
public List<String> getClientAttestationSigningAlgValuesSupported() {
return clientAttestationSigningAlgValuesSupported;
}
public void setClientAttestationSigningAlgValuesSupported(List<String> clientAttestationSigningAlgValuesSupported) {
this.clientAttestationSigningAlgValuesSupported = clientAttestationSigningAlgValuesSupported;
}
public List<String> getClientAttestationPopSigningAlgValuesSupported() {
return clientAttestationPopSigningAlgValuesSupported;
}
public void setClientAttestationPopSigningAlgValuesSupported(List<String> clientAttestationPopSigningAlgValuesSupported) {
this.clientAttestationPopSigningAlgValuesSupported = clientAttestationPopSigningAlgValuesSupported;
}
public List<String> getIntrospectionEndpointAuthMethodsSupported() {
return introspectionEndpointAuthMethodsSupported;
}
@@ -47,5 +47,6 @@ public enum AuthenticationFlowError {
DISPLAY_NOT_SUPPORTED,
ACCESS_DENIED,
UNAUTHORIZED_CLIENT,
GENERIC_AUTHENTICATION_ERROR
}
@@ -23,11 +23,15 @@ import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractClientAuthenticator implements ClientAuthenticator, ClientAuthenticatorFactory {
protected final Logger logger = Logger.getLogger(getClass());
@Override
public ClientAuthenticator create() {
return this;
@@ -0,0 +1,111 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication.authenticators.client;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.ws.rs.core.Response;
import org.keycloak.Config;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import static jakarta.ws.rs.core.Response.Status.NOT_IMPLEMENTED;
/**
* Attestation-Based Client Authentication based on Client Attestation JWT and PoP.
* See <a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-attestation-based-client-auth">specs</a> for more details.
*
* @author <a href="mailto:tdiesler@proton.me">Thomas Diesler</a>
*/
public class AttestationBasedClientAuthenticator extends AbstractClientAuthenticator implements EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "client-attestation";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
Response errorResponse = ClientAuthUtil.errorResponse(NOT_IMPLEMENTED.getStatusCode(), OAuthErrorException.UNAUTHORIZED_CLIENT,
"Attestation-Based Client Authentication not (yet) supported");
context.failure(AuthenticationFlowError.UNAUTHORIZED_CLIENT, errorResponse);
}
@Override
public String getDisplayType() {
return "Attestation-Based";
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getHelpText() {
return "Validates client based on a Client Attestation JWT and a PoP JWT which proves possession of the private key";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}
@Override
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
return List.of();
}
@Override
public Map<String, Object> getAdapterConfiguration(KeycloakSession session, ClientModel client) {
return Map.of();
}
@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_AUTH_ABCA);
}
@Override
public Set<String> getProtocolAuthenticatorMethods(String loginProtocol) {
if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
return Set.of(OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH);
} else {
return Set.of();
}
}
}
@@ -26,12 +26,9 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.resources.IdentityBrokerService;
import org.jboss.logging.Logger;
public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator implements EnvironmentDependentProviderFactory {
private static final Logger LOGGER = Logger.getLogger(FederatedJWTClientAuthenticator.class);
public static final String PROVIDER_ID = "federated-jwt";
public static final String JWT_CREDENTIAL_ISSUER_KEY = "jwt.credential.issuer";
@@ -113,7 +110,7 @@ public class FederatedJWTClientAuthenticator extends AbstractClientAuthenticator
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
}
} catch (Exception e) {
LOGGER.warn("Authentication failed", e);
logger.warn("Authentication failed", e);
context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
}
}
@@ -28,8 +28,6 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.x509.X509ClientCertificateLookup;
import org.jboss.logging.Logger;
public class X509ClientAuthenticator extends AbstractClientAuthenticator {
public static final String PROVIDER_ID = "client-x509";
@@ -56,8 +54,6 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
CUSTOM_OIDS_REVERSED.put("E", "1.2.840.113549.1.9.1"); // Another synonym for "EMAILADDRESS"
}
private final static Logger logger = Logger.getLogger(X509ClientAuthenticator.class);
@Override
public void authenticateClient(ClientAuthenticationFlowContext context) {
@@ -121,6 +121,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String CLIENT_SECRET_JWT = "client_secret_jwt";
public static final String PRIVATE_KEY_JWT = "private_key_jwt";
public static final String TLS_CLIENT_AUTH = "tls_client_auth";
public static final String ATTEST_JWT_CLIENT_AUTH = "attest_jwt_client_auth";
/**
* This is just for legacy setups which expect an unencoded, non-RFC6749 compliant client secret send from Keycloak to an IdP.
@@ -69,6 +69,8 @@ import org.keycloak.urls.UrlType;
import org.keycloak.util.JsonSerialization;
import org.keycloak.wellknown.WellKnownProvider;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -153,10 +155,18 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setPromptValuesSupported(getPromptValuesSupported(realm));
config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
config.setTokenEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
config.setIntrospectionEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
config.setIntrospectionEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
List<String> clientAuthMethodsSupported = getClientAuthMethodsSupported();
List<String> supportedClientSigningAlgorithms = getSupportedClientSigningAlgorithms(false);
config.setTokenEndpointAuthMethodsSupported(clientAuthMethodsSupported);
config.setTokenEndpointAuthSigningAlgValuesSupported(supportedClientSigningAlgorithms);
config.setIntrospectionEndpointAuthMethodsSupported(clientAuthMethodsSupported);
config.setIntrospectionEndpointAuthSigningAlgValuesSupported(supportedClientSigningAlgorithms);
if (clientAuthMethodsSupported.contains(ATTEST_JWT_CLIENT_AUTH)) {
config.setClientAttestationSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
config.setClientAttestationPopSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
}
config.setAuthorizationSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
config.setAuthorizationEncryptionAlgValuesSupported(getSupportedEncryptionAlg(false));
@@ -199,8 +209,8 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
// NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider
// is not exposed over "http" at all.
config.setRevocationEndpoint(revocationEndpoint.toString());
config.setRevocationEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
config.setRevocationEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
config.setRevocationEndpointAuthMethodsSupported(clientAuthMethodsSupported);
config.setRevocationEndpointAuthSigningAlgValuesSupported(supportedClientSigningAlgorithms);
config.setBackchannelLogoutSupported(true);
config.setBackchannelLogoutSessionSupported(true);
@@ -249,11 +259,12 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
}
private List<String> getClientAuthMethodsSupported() {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientAuthenticator.class)
List<String> clientAuthMethods = session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientAuthenticator.class)
.map(ClientAuthenticatorFactory.class::cast)
.map(caf -> caf.getProtocolAuthenticatorMethods(OIDCLoginProtocol.LOGIN_PROTOCOL))
.flatMap(Collection::stream)
.collect(Collectors.toList());
return clientAuthMethods;
}
private List<String> getSupportedAlgorithms(Class<? extends Provider> clazz, boolean includeNone) {
@@ -19,4 +19,5 @@ org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator
org.keycloak.authentication.authenticators.client.JWTClientAuthenticator
org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator
org.keycloak.authentication.authenticators.client.X509ClientAuthenticator
org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator
org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator
org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator
@@ -404,6 +404,13 @@ public abstract class OID4VCIssuerTestBase {
}
}
public static class VCTestServerWithABCAEnabled implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OID4VC_VCI, Profile.Feature.CLIENT_AUTH_ABCA);
}
}
public static class VCTestRealmConfig implements RealmConfig {
public static final String TEST_REALM_NAME = "test";
@@ -0,0 +1,40 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.oid4vc;
import java.util.List;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.junit.jupiter.api.Test;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH;
import static org.junit.jupiter.api.Assertions.assertFalse;
@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerConfig.class)
public class OIDCAuthorizationMetadataTest extends OID4VCIssuerTestBase {
@Test
public void testTokenEndpointAuthMethods() {
OIDCConfigurationRepresentation oidcConfiguration = oauth.doWellKnownRequest();
List<String> tokenAuthMethodsSupported = oidcConfiguration.getTokenEndpointAuthMethodsSupported();
assertFalse(tokenAuthMethodsSupported.contains(ATTEST_JWT_CLIENT_AUTH), "Should not contain: " + ATTEST_JWT_CLIENT_AUTH);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.oid4vc.abca;
import java.util.List;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.tests.oid4vc.OID4VCIssuerTestBase;
import org.junit.jupiter.api.Test;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerWithABCAEnabled.class)
public class OIDCAuthorizationMetadataABCATest extends OID4VCIssuerTestBase {
@Test
public void testTokenEndpointAuthMethods() {
OIDCConfigurationRepresentation oidcConfiguration = oauth.doWellKnownRequest();
List<String> tokenAuthMethodsSupported = oidcConfiguration.getTokenEndpointAuthMethodsSupported();
assertTrue(tokenAuthMethodsSupported.contains(ATTEST_JWT_CLIENT_AUTH), "Should contain: " + ATTEST_JWT_CLIENT_AUTH);
}
}