Add X.509 authenticator option for canonical DN

Because the current distinguished name determination is security provider
dependent, a new authenticator option is added to use the canonical format
of the distinguished name, as descriped in
javax.security.auth.x500.X500Principal.getName(String format).
This commit is contained in:
Sebastian Loesch 2019-02-12 13:44:18 +01:00 committed by Hynek Mlnařík
parent 7a671052a3
commit 43393220bf
7 changed files with 233 additions and 14 deletions

View file

@ -23,6 +23,7 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.function.Function; import java.util.function.Function;
import javax.security.auth.x500.X500Principal;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.bouncycastle.asn1.x500.X500Name; 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_CRL = "x509-cert-auth.crl-checking-enabled";
public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-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 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 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_URI = "x509-cert-auth.ocsp-responder-uri";
public static final String OCSPRESPONDER_CERTIFICATE = "x509-cert-auth.ocsp-responder-certificate"; 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(); String pattern = config.getRegularExpression();
UserIdentityExtractor extractor = null; UserIdentityExtractor extractor = null;
Function<X509Certificate[], String> func = null;
switch(userIdentitySource) { switch(userIdentitySource) {
case SUBJECTDN: 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; break;
case ISSUERDN: 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; break;
case SERIALNUMBER: case SERIALNUMBER:
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> certs[0].getSerialNumber().toString()); extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> certs[0].getSerialNumber().toString());

View file

@ -29,7 +29,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger; 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.CERTIFICATE_KEY_USAGE;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CONFIRMATION_PAGE_DISALLOWED; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CONFIRMATION_PAGE_DISALLOWED;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CRL_RELATIVE_PATH; 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.setDefaultValue(mappingSources[0]);
mappingMethodList.setOptions(mappingSourceTypes); 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(); ProviderConfigProperty regExp = new ProviderConfigProperty();
regExp.setType(STRING_TYPE); regExp.setType(STRING_TYPE);
regExp.setName(REGULAR_EXPRESSION); 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."); 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, configProperties = asList(mappingMethodList,
canonicalDn,
regExp, regExp,
userMapperList, userMapperList,
attributeOrPropertyValue, attributeOrPropertyValue,

View file

@ -244,4 +244,12 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
return this; 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;
}
} }

View file

@ -20,6 +20,7 @@ package org.keycloak.testsuite.x509;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; 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.AppPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage; import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage;
import org.keycloak.testsuite.updaters.SetSystemProperty;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents; import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.testsuite.util.ClientBuilder; 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.ISSUERDN_CN;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTALTNAME_EMAIL; 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.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_CN;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
@ -106,6 +109,8 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
protected AuthenticationExecutionInfoRepresentation directGrantExecution; protected AuthenticationExecutionInfoRepresentation directGrantExecution;
private static SetSystemProperty phantomjsCliArgs;
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@ -141,6 +146,10 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
configurePhantomJS("/ca.crt", "/client.crt", "/client.key", "password"); 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" * 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-file=").append(authServerHome).append(clientKeyFile).append(" ");
cliArgs.append("--ssl-client-key-passphrase=" + clientKeyPassword).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"); .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) { protected void setUserEnabled(String userName, boolean enabled) {
UserRepresentation user = findUser(userName); UserRepresentation user = findUser(userName);
Assert.assertNotNull(user); Assert.assertNotNull(user);

View file

@ -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);
}
}

View file

@ -18,18 +18,9 @@
package org.keycloak.testsuite.x509; package org.keycloak.testsuite.x509;
import org.jboss.arquillian.drone.api.annotation.Drone; 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.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; 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.keycloak.testsuite.util.PhantomJSBrowser;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
@ -56,7 +47,6 @@ public class X509BrowserLoginSubjectAltNameTest extends AbstractX509Authenticati
"/certs/clients/test-user@localhost.key.pem", "password"); "/certs/clients/test-user@localhost.key.pem", "password");
} }
@Test @Test
public void loginAsUserFromCertSANEmail() { public void loginAsUserFromCertSANEmail() {
x509BrowserLogin(createLoginSubjectAltNameEmail2UserAttributeConfig(), userId, "test-user@localhost", "test-user-altmail@localhost"); x509BrowserLogin(createLoginSubjectAltNameEmail2UserAttributeConfig(), userId, "test-user@localhost", "test-user-altmail@localhost");

View file

@ -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);
}
}