From d710746db6e74f9da3518246f65c8a7185e012d8 Mon Sep 17 00:00:00 2001 From: Thomas Diesler Date: Mon, 16 Mar 2026 14:30:47 +0100 Subject: [PATCH] [OID4VCI-HAIP] Pass oid4vci-1_0-issuer-metadata_test Signed-off-by: Thomas Diesler --- .../java/org/keycloak/common/Profile.java | 1 + .../OIDCConfigurationRepresentation.java | 22 ++++ .../AuthenticationFlowError.java | 1 + .../client/AbstractClientAuthenticator.java | 4 + .../AttestationBasedClientAuthenticator.java | 111 ++++++++++++++++++ .../FederatedJWTClientAuthenticator.java | 5 +- .../client/X509ClientAuthenticator.java | 4 - .../protocol/oidc/OIDCLoginProtocol.java | 1 + .../protocol/oidc/OIDCWellKnownProvider.java | 25 ++-- ....authentication.ClientAuthenticatorFactory | 3 +- .../tests/oid4vc/OID4VCIssuerTestBase.java | 7 ++ .../oid4vc/OIDCAuthorizationMetadataTest.java | 40 +++++++ .../OIDCAuthorizationMetadataABCATest.java | 40 +++++++ 13 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/client/AttestationBasedClientAuthenticator.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/oid4vc/OIDCAuthorizationMetadataTest.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCAuthorizationMetadataABCATest.java diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index d42a6145fcce..085b24925d32 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -98,6 +98,7 @@ public enum Feature { 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), diff --git a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java index 074b9a708026..8678abdcc508 100755 --- a/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -115,6 +115,12 @@ public class OIDCConfigurationRepresentation { @JsonProperty("token_endpoint_auth_signing_alg_values_supported") private List tokenEndpointAuthSigningAlgValuesSupported; + @JsonProperty("client_attestation_signing_alg_values_supported") + private List clientAttestationSigningAlgValuesSupported; + + @JsonProperty("client_attestation_pop_signing_alg_values_supported") + private List clientAttestationPopSigningAlgValuesSupported; + @JsonProperty("introspection_endpoint_auth_methods_supported") private List introspectionEndpointAuthMethodsSupported; @@ -410,6 +416,22 @@ public void setTokenEndpointAuthSigningAlgValuesSupported(List tokenEndp this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported; } + public List getClientAttestationSigningAlgValuesSupported() { + return clientAttestationSigningAlgValuesSupported; + } + + public void setClientAttestationSigningAlgValuesSupported(List clientAttestationSigningAlgValuesSupported) { + this.clientAttestationSigningAlgValuesSupported = clientAttestationSigningAlgValuesSupported; + } + + public List getClientAttestationPopSigningAlgValuesSupported() { + return clientAttestationPopSigningAlgValuesSupported; + } + + public void setClientAttestationPopSigningAlgValuesSupported(List clientAttestationPopSigningAlgValuesSupported) { + this.clientAttestationPopSigningAlgValuesSupported = clientAttestationPopSigningAlgValuesSupported; + } + public List getIntrospectionEndpointAuthMethodsSupported() { return introspectionEndpointAuthMethodsSupported; } diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java index 612b859e563a..869f3e7b1e58 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java @@ -47,5 +47,6 @@ public enum AuthenticationFlowError { DISPLAY_NOT_SUPPORTED, ACCESS_DENIED, + UNAUTHORIZED_CLIENT, GENERIC_AUTHENTICATION_ERROR } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java index 516df6e00eaf..238b80587880 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/AbstractClientAuthenticator.java @@ -23,11 +23,15 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.jboss.logging.Logger; + /** * @author Marek Posolda */ public abstract class AbstractClientAuthenticator implements ClientAuthenticator, ClientAuthenticatorFactory { + protected final Logger logger = Logger.getLogger(getClass()); + @Override public ClientAuthenticator create() { return this; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/AttestationBasedClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/AttestationBasedClientAuthenticator.java new file mode 100644 index 000000000000..1738d4d8b082 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/AttestationBasedClientAuthenticator.java @@ -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 specs for more details. + * + * @author Thomas Diesler + */ +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 getConfigProperties() { + return List.of(); + } + + @Override + public List getConfigPropertiesPerClient() { + return List.of(); + } + + @Override + public Map 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 getProtocolAuthenticatorMethods(String loginProtocol) { + if (loginProtocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) { + return Set.of(OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH); + } else { + return Set.of(); + } + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/FederatedJWTClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/FederatedJWTClientAuthenticator.java index fad9283cfbf8..f2a94dae0bac 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/FederatedJWTClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/FederatedJWTClientAuthenticator.java @@ -26,12 +26,9 @@ 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 void authenticateClient(ClientAuthenticationFlowContext context) { 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); } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java index 661469429e8d..dc80b859bbba 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/X509ClientAuthenticator.java @@ -28,8 +28,6 @@ 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) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 286296fcdfe6..37dc613d10e9 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -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. diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index ba196502d547..b9af5336f014 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -69,6 +69,8 @@ import org.keycloak.util.JsonSerialization; import org.keycloak.wellknown.WellKnownProvider; +import static org.keycloak.protocol.oidc.OIDCLoginProtocol.ATTEST_JWT_CLIENT_AUTH; + /** * @author Stian Thorgersen */ @@ -153,10 +155,18 @@ public Object getConfig() { config.setPromptValuesSupported(getPromptValuesSupported(realm)); - config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); - config.setTokenEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false)); - config.setIntrospectionEndpointAuthMethodsSupported(getClientAuthMethodsSupported()); - config.setIntrospectionEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false)); + List clientAuthMethodsSupported = getClientAuthMethodsSupported(); + List 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 Object getConfig() { // 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 @@ private static List list(String... values) { } private List getClientAuthMethodsSupported() { - return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientAuthenticator.class) + List 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 getSupportedAlgorithms(Class clazz, boolean includeNone) { diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory index c0b35f485d72..192fd787b294 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory @@ -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 \ No newline at end of file +org.keycloak.authentication.authenticators.client.FederatedJWTClientAuthenticator +org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCIssuerTestBase.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCIssuerTestBase.java index 9846a42bf09b..833aca0b2ae3 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCIssuerTestBase.java +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCIssuerTestBase.java @@ -404,6 +404,13 @@ public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) } } + 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"; diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OIDCAuthorizationMetadataTest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OIDCAuthorizationMetadataTest.java new file mode 100644 index 000000000000..ec1e669549ed --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OIDCAuthorizationMetadataTest.java @@ -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 tokenAuthMethodsSupported = oidcConfiguration.getTokenEndpointAuthMethodsSupported(); + assertFalse(tokenAuthMethodsSupported.contains(ATTEST_JWT_CLIENT_AUTH), "Should not contain: " + ATTEST_JWT_CLIENT_AUTH); + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCAuthorizationMetadataABCATest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCAuthorizationMetadataABCATest.java new file mode 100644 index 000000000000..99f6b745b484 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/abca/OIDCAuthorizationMetadataABCATest.java @@ -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 tokenAuthMethodsSupported = oidcConfiguration.getTokenEndpointAuthMethodsSupported(); + assertTrue(tokenAuthMethodsSupported.contains(ATTEST_JWT_CLIENT_AUTH), "Should contain: " + ATTEST_JWT_CLIENT_AUTH); + } +}