diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 930cea310a..b410ff88a1 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -23,6 +23,7 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.function.Function; +import javax.security.auth.x500.X500Principal; import javax.ws.rs.core.Response; import org.bouncycastle.asn1.x500.X500Name; @@ -52,6 +53,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled"; public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled"; public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled"; + public static final String CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled"; public static final String CRL_RELATIVE_PATH = "x509-cert-auth.crl-relative-path"; public static final String OCSPRESPONDER_URI = "x509-cert-auth.ocsp-responder-uri"; public static final String OCSPRESPONDER_CERTIFICATE = "x509-cert-auth.ocsp-responder-certificate"; @@ -131,13 +133,20 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth String pattern = config.getRegularExpression(); UserIdentityExtractor extractor = null; + Function func = null; switch(userIdentitySource) { case SUBJECTDN: - extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, certs -> certs[0].getSubjectDN().getName()); + func = config.isCanonicalDnEnabled() ? + certs -> certs[0].getSubjectX500Principal().getName(X500Principal.CANONICAL) : + certs -> certs[0].getSubjectDN().getName(); + extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, func); break; case ISSUERDN: - extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, certs -> certs[0].getIssuerDN().getName()); + func = config.isCanonicalDnEnabled() ? + certs -> certs[0].getIssuerX500Principal().getName(X500Principal.CANONICAL) : + certs -> certs[0].getIssuerDN().getName(); + extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, func); break; case SERIALNUMBER: extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> certs[0].getSerialNumber().toString()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java index ae531b8a2b..d99ee1b0e6 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java @@ -29,7 +29,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.services.ServicesLogger; -import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_EXTENDED_KEY_USAGE; +import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.*; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_KEY_USAGE; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CONFIRMATION_PAGE_DISALLOWED; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CRL_RELATIVE_PATH; @@ -101,6 +101,13 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen mappingMethodList.setDefaultValue(mappingSources[0]); mappingMethodList.setOptions(mappingSourceTypes); + ProviderConfigProperty canonicalDn = new ProviderConfigProperty(); + canonicalDn.setType(BOOLEAN_TYPE); + canonicalDn.setName(CANONICAL_DN); + canonicalDn.setLabel("Canonical DN representation enabled"); + canonicalDn.setDefaultValue(false); + canonicalDn.setHelpText("Use the canonical format to determine the distinguished name. This option is relevant for authenticators using a distinguished name."); + ProviderConfigProperty regExp = new ProviderConfigProperty(); regExp.setType(STRING_TYPE); regExp.setName(REGULAR_EXPRESSION); @@ -189,6 +196,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen identityConfirmationPageDisallowed.setHelpText("By default, the users are prompted to confirm their identity extracted from X509 client certificate. The identity confirmation prompt is skipped if the option is switched on."); configProperties = asList(mappingMethodList, + canonicalDn, regExp, userMapperList, attributeOrPropertyValue, diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java index ae50667663..ac57296057 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java @@ -244,4 +244,12 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel { return this; } + public boolean isCanonicalDnEnabled() { + return Boolean.parseBoolean(getConfig().get(CANONICAL_DN)); + } + + public X509AuthenticatorConfigModel setCanonicalDnEnabled(boolean value) { + getConfig().put(CANONICAL_DN, Boolean.toString(value)); + return this; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java index facae2829b..e76bafb58b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.x509; import org.jboss.arquillian.graphene.page.Page; import org.jboss.logging.Logger; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; @@ -50,6 +51,7 @@ import org.keycloak.testsuite.pages.AbstractPage; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage; +import org.keycloak.testsuite.updaters.SetSystemProperty; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AssertAdminEvents; import org.keycloak.testsuite.util.ClientBuilder; @@ -74,6 +76,7 @@ import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorC import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN_CN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTALTNAME_EMAIL; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTALTNAME_OTHERNAME; +import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_CN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL; @@ -106,6 +109,8 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe protected AuthenticationExecutionInfoRepresentation directGrantExecution; + private static SetSystemProperty phantomjsCliArgs; + @Rule public AssertEvents events = new AssertEvents(this); @@ -141,6 +146,10 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe configurePhantomJS("/ca.crt", "/client.crt", "/client.key", "password"); } + @AfterClass + public static void onAfterTestClass() { + phantomjsCliArgs.revert(); + } /** * Setup phantom JS to be used for mutual TLS testing. All file paths are relative to "authServerHome" @@ -163,7 +172,7 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe cliArgs.append("--ssl-client-key-file=").append(authServerHome).append(clientKeyFile).append(" "); cliArgs.append("--ssl-client-key-passphrase=" + clientKeyPassword).append(" "); - System.setProperty("keycloak.phantomjs.cli.args", cliArgs.toString()); + phantomjsCliArgs = new SetSystemProperty("keycloak.phantomjs.cli.args", cliArgs.toString()); } } @@ -442,6 +451,26 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe .setCustomAttributeName("x509_certificate_identity"); } + protected static X509AuthenticatorConfigModel createLoginSubjectDNToCustomAttributeConfig(boolean canonicalDnEnabled) { + return new X509AuthenticatorConfigModel() + .setConfirmationPageAllowed(true) + .setCanonicalDnEnabled(canonicalDnEnabled) + .setMappingSourceType(SUBJECTDN) + .setRegularExpression("(.*?)(?:$)") + .setUserIdentityMapperType(USER_ATTRIBUTE) + .setCustomAttributeName("x509_certificate_identity"); + } + + protected static X509AuthenticatorConfigModel createLoginIssuerDNToCustomAttributeConfig(boolean canonicalDnEnabled) { + return new X509AuthenticatorConfigModel() + .setConfirmationPageAllowed(true) + .setCanonicalDnEnabled(canonicalDnEnabled) + .setMappingSourceType(ISSUERDN) + .setRegularExpression("(.*?)(?:$)") + .setUserIdentityMapperType(USER_ATTRIBUTE) + .setCustomAttributeName("x509_certificate_identity"); + } + protected void setUserEnabled(String userName, boolean enabled) { UserRepresentation user = findUser(userName); Assert.assertNotNull(user); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginIssuerDnTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginIssuerDnTest.java new file mode 100644 index 0000000000..5e9d62b796 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginIssuerDnTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019 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.testsuite.x509; + +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.PhantomJSBrowser; +import org.openqa.selenium.WebDriver; + + +/** + * @author Sebastian Loesch + * @date 02/14/2019 + */ + +public class X509BrowserLoginIssuerDnTest extends AbstractX509AuthenticationTest { + + @Drone + @PhantomJSBrowser + private WebDriver phantomJS; + + @Before + public void replaceTheDefaultDriver() { + replaceDefaultWebDriver(phantomJS); + } + + @BeforeClass + public static void checkAssumption() { + try { + CertificateFactory.getInstance("X.509", "SUN"); + } + catch (CertificateException | NoSuchProviderException e) { + Assume.assumeNoException("Test assumes the SUN security provider", e); + } + } + + @BeforeClass + public static void onBeforeTestClass() { + configurePhantomJS("/ca.crt", "/certs/clients/test-user-san@localhost.cert.pem", + "/certs/clients/test-user@localhost.key.pem", "password"); + } + + private String setup(boolean canonicalDnEnabled) throws Exception { + String issuerDn = canonicalDnEnabled ? + "1.2.840.113549.1.9.1=#1614636f6e74616374406b6579636c6f616b2e6f7267,cn=keycloak intermediate ca,ou=keycloak,o=red hat,st=ma,c=us" : + "EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US"; + + UserRepresentation user = findUser("test-user@localhost"); + user.singleAttribute("x509_certificate_identity", issuerDn); + updateUser(user); + return issuerDn; + } + + @Test + public void loginAsUserFromCertIssuerDnCanonical() throws Exception { + String issuerDn = setup(true); + x509BrowserLogin(createLoginIssuerDNToCustomAttributeConfig(true), userId, "test-user@localhost", issuerDn); + } + + @Test + public void loginAsUserFromCertIssuerDnNonCanonical() throws Exception { + String issuerDn = setup(false); + x509BrowserLogin(createLoginIssuerDNToCustomAttributeConfig(false), userId, "test-user@localhost", issuerDn); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectAltNameTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectAltNameTest.java index 41dcbee858..8be212dced 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectAltNameTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectAltNameTest.java @@ -18,18 +18,9 @@ package org.keycloak.testsuite.x509; import org.jboss.arquillian.drone.api.annotation.Drone; -import org.jboss.arquillian.graphene.page.Page; -import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.keycloak.OAuth2Constants; -import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel; -import org.keycloak.events.Details; -import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; -import org.keycloak.testsuite.pages.AppPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage; import org.keycloak.testsuite.util.PhantomJSBrowser; import org.openqa.selenium.WebDriver; @@ -56,7 +47,6 @@ public class X509BrowserLoginSubjectAltNameTest extends AbstractX509Authenticati "/certs/clients/test-user@localhost.key.pem", "password"); } - @Test public void loginAsUserFromCertSANEmail() { x509BrowserLogin(createLoginSubjectAltNameEmail2UserAttributeConfig(), userId, "test-user@localhost", "test-user-altmail@localhost"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectDnTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectDnTest.java new file mode 100644 index 0000000000..7d95c66ef1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginSubjectDnTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019 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.testsuite.x509; + +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; + +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.PhantomJSBrowser; +import org.openqa.selenium.WebDriver; + +/** + * @author Sebastian Loesch + * @date 02/14/2019 + */ + +public class X509BrowserLoginSubjectDnTest extends AbstractX509AuthenticationTest { + + @Drone + @PhantomJSBrowser + private WebDriver phantomJS; + + @Before + public void replaceTheDefaultDriver() { + replaceDefaultWebDriver(phantomJS); + } + + @BeforeClass + public static void checkAssumption() { + try { + CertificateFactory.getInstance("X.509", "SUN"); + } + catch (CertificateException | NoSuchProviderException e) { + Assume.assumeNoException("Test assumes the SUN security provider", e); + } + } + + @BeforeClass + public static void onBeforeTestClass() { + configurePhantomJS("/ca.crt", "/certs/clients/test-user-san@localhost.cert.pem", + "/certs/clients/test-user@localhost.key.pem", "password"); + } + + private String setup(boolean canonicalDnEnabled) throws Exception { + String subjectDn = canonicalDnEnabled ? + "1.2.840.113549.1.9.1=#1613746573742d75736572406c6f63616c686f7374,cn=test-user,ou=keycloak,o=red hat,l=boston,st=ma,c=us" : + "EMAILADDRESS=test-user@localhost, CN=test-user, OU=Keycloak, O=Red Hat, L=Boston, ST=MA, C=US"; + + UserRepresentation user = findUser("test-user@localhost"); + user.singleAttribute("x509_certificate_identity",subjectDn); + updateUser(user); + return subjectDn; + } + + @Test + public void loginAsUserFromCertSubjectDnCanonical() throws Exception { + String subjectDn = setup(true); + x509BrowserLogin(createLoginSubjectDNToCustomAttributeConfig(true), userId, "test-user@localhost", subjectDn); + } + + @Test + public void loginAsUserFromCertSubjectDnNonCanonical() throws Exception { + String subjectDn = setup(false); + x509BrowserLogin(createLoginSubjectDNToCustomAttributeConfig(false), userId, "test-user@localhost", subjectDn); + } +} \ No newline at end of file