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:
parent
7a671052a3
commit
43393220bf
7 changed files with 233 additions and 14 deletions
|
@ -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<X509Certificate[], String> 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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue