Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
Comment thread
tdiesler marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -410,6 +416,22 @@ public void setTokenEndpointAuthSigningAlgValuesSupported(List<String> tokenEndp
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ public enum AuthenticationFlowError {
DISPLAY_NOT_SUPPORTED,

ACCESS_DENIED,
UNAUTHORIZED_CLIENT,
GENERIC_AUTHENTICATION_ERROR
}
Comment thread
tdiesler marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

import org.jboss.logging.Logger;

/**
* @author <a href="mailto:[email protected]">Marek Posolda</a>
*/
public abstract class AbstractClientAuthenticator implements ClientAuthenticator, ClientAuthenticatorFactory {

protected final Logger logger = Logger.getLogger(getClass());

@Override
public ClientAuthenticator create() {
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -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:[email protected]">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);
}
Comment thread
tdiesler marked this conversation as resolved.

@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();
}
Comment thread
tdiesler marked this conversation as resolved.

@Override
public List<ProviderConfigProperty> getConfigPropertiesPerClient() {
return List.of();
}

@Override
public Map<String, Object> getAdapterConfiguration(KeycloakSession session, ClientModel client) {
return Map.of();
}

Comment thread
tdiesler marked this conversation as resolved.
@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();
}
}
}
Comment thread
tdiesler marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="mailto:[email protected]">Stian Thorgersen</a>
*/
Expand Down Expand Up @@ -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<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));
}
Comment thread
tdiesler marked this conversation as resolved.

config.setAuthorizationSigningAlgValuesSupported(getSupportedSigningAlgorithms(false));
config.setAuthorizationEncryptionAlgValuesSupported(getSupportedEncryptionAlg(false));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -249,11 +259,12 @@ private static List<String> list(String... values) {
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Comment thread
tdiesler marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading