KEYCLOAK-6056 Map user by Subject Alternative Name (otherName) when authenticating user with X509

This commit is contained in:
mposolda 2019-03-07 08:22:53 +01:00 committed by Marek Posolda
parent cf35a4648b
commit a48698caa3
18 changed files with 374 additions and 92 deletions

View file

@ -59,6 +59,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
public static final String MAPPING_SOURCE_CERT_SUBJECTDN = "Match SubjectDN using regular expression"; public static final String MAPPING_SOURCE_CERT_SUBJECTDN = "Match SubjectDN using regular expression";
public static final String MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL = "Subject's e-mail"; public static final String MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL = "Subject's e-mail";
public static final String MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL = "Subject's Alternative Name E-mail"; public static final String MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL = "Subject's Alternative Name E-mail";
public static final String MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME = "Subject's Alternative Name otherName (UPN)";
public static final String MAPPING_SOURCE_CERT_SUBJECTDN_CN = "Subject's Common Name"; public static final String MAPPING_SOURCE_CERT_SUBJECTDN_CN = "Subject's Common Name";
public static final String MAPPING_SOURCE_CERT_ISSUERDN = "Match IssuerDN using regular expression"; public static final String MAPPING_SOURCE_CERT_ISSUERDN = "Match IssuerDN using regular expression";
public static final String MAPPING_SOURCE_CERT_ISSUERDN_EMAIL = "Issuer's e-mail"; public static final String MAPPING_SOURCE_CERT_ISSUERDN_EMAIL = "Issuer's e-mail";
@ -152,6 +153,9 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
case SUBJECTALTNAME_EMAIL: case SUBJECTALTNAME_EMAIL:
extractor = UserIdentityExtractor.getSubjectAltNameExtractor(1); extractor = UserIdentityExtractor.getSubjectAltNameExtractor(1);
break; break;
case SUBJECTALTNAME_OTHERNAME:
extractor = UserIdentityExtractor.getSubjectAltNameExtractor(0);
break;
case ISSUERDN_CN: case ISSUERDN_CN:
extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, issuer); extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, issuer);
break; break;

View file

@ -44,6 +44,7 @@ import static org.keycloak.authentication.authenticators.x509.AbstractX509Client
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_ISSUERDN_EMAIL; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_ISSUERDN_EMAIL;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SERIALNUMBER; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SERIALNUMBER;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_CN; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_CN;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL; import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL;
@ -72,6 +73,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
MAPPING_SOURCE_CERT_SUBJECTDN, MAPPING_SOURCE_CERT_SUBJECTDN,
MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL, MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL,
MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL, MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL,
MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME,
MAPPING_SOURCE_CERT_SUBJECTDN_CN, MAPPING_SOURCE_CERT_SUBJECTDN_CN,
MAPPING_SOURCE_CERT_ISSUERDN, MAPPING_SOURCE_CERT_ISSUERDN,
MAPPING_SOURCE_CERT_ISSUERDN_EMAIL, MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,

View file

@ -19,12 +19,18 @@
package org.keycloak.authentication.authenticators.x509; package org.keycloak.authentication.authenticators.x509;
import freemarker.template.utility.NullArgumentException; import freemarker.template.utility.NullArgumentException;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateParsingException; import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Collection; import java.util.Collection;
@ -101,6 +107,9 @@ public abstract class UserIdentityExtractor {
*/ */
static class SubjectAltNameExtractor extends UserIdentityExtractor { static class SubjectAltNameExtractor extends UserIdentityExtractor {
// User Principal Name. Used typically by Microsoft in certificates for Smart Card Login
private static final String UPN_OID = "1.3.6.1.4.1.311.20.2.3";
private final int generalName; private final int generalName;
/** /**
@ -127,19 +136,79 @@ public abstract class UserIdentityExtractor {
Iterator<List<?>> iterator = subjectAlternativeNames.iterator(); Iterator<List<?>> iterator = subjectAlternativeNames.iterator();
while (iterator.hasNext()) { boolean foundUpn = false;
String tempOtherName = null;
String tempOid = null;
while (iterator.hasNext() && !foundUpn) {
List<?> next = iterator.next(); List<?> next = iterator.next();
if (Integer.class.cast(next.get(0)) == generalName) { if (Integer.class.cast(next.get(0)) == generalName) {
return next.get(1);
// We will try to find UPN_OID among the subjectAltNames of type 'otherName' . Just if not found, we will fallback to the other type
for (int i = 1 ; i<next.size() ; i++) {
Object obj = next.get(i);
// We have Subject Alternative Name of other type than 'otherName' . Just return it directly
if (generalName != 0) {
logger.tracef("Extracted identity '%s' from Subject Alternative Name of type '%d'", obj, generalName);
return obj;
}
byte[] otherNameBytes = (byte[]) obj;
try {
ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes));
ASN1Encodable asn1otherName = asn1Stream.readObject();
asn1otherName = unwrap(asn1otherName);
ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName);
if (asn1Sequence != null) {
ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0);
ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid));
tempOid = oid.getId();
ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1);
DERUTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded));
tempOtherName = principalName.getString();
// We found UPN among the 'otherName' principal. We don't need to look other
if (UPN_OID.equals(tempOid)) {
foundUpn = true;
break;
} }
} }
} catch (Exception e) {
logger.error("Failed to parse subjectAltName", e);
}
}
}
}
logger.tracef("Parsed otherName from subjectAltName. OID: '%s', Principal: '%s'", tempOid, tempOtherName);
return tempOtherName;
} catch (CertificateParsingException cause) { } catch (CertificateParsingException cause) {
logger.errorf(cause, "Failed to obtain identity from subjectAltName extension"); logger.errorf(cause, "Failed to obtain identity from subjectAltName extension");
} }
return null; return null;
} }
private ASN1Encodable unwrap(ASN1Encodable encodable) {
while (encodable instanceof ASN1TaggedObject) {
ASN1TaggedObject taggedObj = (ASN1TaggedObject) encodable;
encodable = taggedObj.getObject();
}
return encodable;
}
} }
static class PatternMatcher extends UserIdentityExtractor { static class PatternMatcher extends UserIdentityExtractor {

View file

@ -61,6 +61,7 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
SUBJECTDN_CN(MAPPING_SOURCE_CERT_SUBJECTDN_CN), SUBJECTDN_CN(MAPPING_SOURCE_CERT_SUBJECTDN_CN),
SUBJECTDN_EMAIL(MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL), SUBJECTDN_EMAIL(MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL),
SUBJECTALTNAME_EMAIL(MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL), SUBJECTALTNAME_EMAIL(MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL),
SUBJECTALTNAME_OTHERNAME(MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME),
SUBJECTDN(MAPPING_SOURCE_CERT_SUBJECTDN); SUBJECTDN(MAPPING_SOURCE_CERT_SUBJECTDN);
private String name; private String name;

View file

@ -0,0 +1,64 @@
/*
* Copyright 2017 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.x509;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.cert.X509Certificate;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.StreamUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SubjectAltNameIdentityExtractorTest {
@Test
public void testX509SubjectAltName_otherName() throws Exception {
UserIdentityExtractor extractor = UserIdentityExtractor.getSubjectAltNameExtractor(0);
X509Certificate cert = getCertificate();
Object upn = extractor.extractUserIdentity(new X509Certificate[] { cert});
Assert.assertEquals("test-user@some-company-domain", upn);
}
@Test
public void testX509SubjectAltName_email() throws Exception {
UserIdentityExtractor extractor = UserIdentityExtractor.getSubjectAltNameExtractor(1);
X509Certificate cert = getCertificate();
Object upn = extractor.extractUserIdentity(new X509Certificate[] { cert});
Assert.assertEquals("test@somecompany.com", upn);
}
private X509Certificate getCertificate() throws Exception {
InputStream is = getClass().getResourceAsStream("/certs/UPN-cert.pem");
String s = StreamUtil.readString(is, Charset.defaultCharset());
return PemUtils.decodeCertificate(s);
}
}

View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE6DCCA9CgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpzELMAkGA1UEBhMCVVMx
HTAbBgNVBAgMFERpc3RyaWN0IG9mIENvbHVtYmlhMRMwEQYDVQQHDApXYXNoaW5n
dG9uMRUwEwYDVQQKDAxTb21lIENvbXBhbnkxGDAWBgNVBAsMD1NvbWUgRGVwYXJ0
bWVudDEQMA4GA1UEAwwHVGVzdCBDQTEhMB8GCSqGSIb3DQEJARYSY2FAc29tZWNv
bXBhbnkuY29tMB4XDTE0MDYxMjE3MDkxMloXDTI0MDYxMjE3MDkxMlowgZYxCzAJ
BgNVBAYTAlVTMR0wGwYDVQQIDBREaXN0cmljdCBvZiBDb2x1bWJpYTEVMBMGA1UE
CgwMU29tZSBDb21wYW55MRgwFgYDVQQLDA9Tb21lIERlcGFydG1lbnQxEjAQBgNV
BAMMCVRlc3QgVXNlcjEjMCEGCSqGSIb3DQEJARYUdGVzdEBzb21lY29tcGFueS5j
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD42s0Q8YXv/rQrk4rF
4aHmuz8Tq9jWk3bU/tJoBgZTG2xCYfolT2z4j2Qa6kjXucEJuqOKihxMMZ1We0G4
I6tm5QJxqkEoUYmUZHu/QZSrH1gwgS0yjvfq+Kk+yvKqplXDUyxbLRuMBRgFRCy0
TUvdJPE4IQZQCcHir0Vqs667vj0UjSpI+y0BDZPY5CRePRSKcjM4ixoR9B8xj5kg
RcMxg4EszC2oK7z0IuuYKi0ZOdot1wVKP4OD/9evE2wjUYVeYCAV9y7tMlVsN0N5
dRplCSIa/CA5gTMod3C92t83VoPqfb0f71cNQAsx1V3dNtOKnTOoG5jX70RR4Rqk
8ItNAgMBAAGjggEsMIIBKDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAL
BgNVHQ8EBAMCBeAwKwYJYIZIAYb4QgENBB4WHFNtYXJ0IENhcmQgTG9naW4gQ2Vy
dGlmaWNhdGUwHQYDVR0OBBYEFN1P5EBNqZ+MGrJQziiVMhkKAXr9MB8GA1UdIwQY
MBaAFCHrFg422S+AWOHXfPIdqxbRhXegME4GA1UdEQRHMEWBFHRlc3RAc29tZWNv
bXBhbnkuY29toC0GCisGAQQBgjcUAgOgHwwddGVzdC11c2VyQHNvbWUtY29tcGFu
eS1kb21haW4wHQYDVR0SBBYwFIESY2FAc29tZWNvbXBhbnkuY29tMB8GA1UdJQQY
MBYGCCsGAQUFBwMCBgorBgEEAYI3FAICMA0GCSqGSIb3DQEBCwUAA4IBAQCPI5Zk
DqGHkKfFhRjlzLLajUEveggs74x3roi6S0zlpXpbPA3iZ2N8COf/QZL3twKunbP9
XpmW/pcSji3+pil9aHPRn69S4cSuKdN5ZP9oQhkZxdk2UFS8Ts0WA+SUDJ3qTEtA
Q0HswBFBzWGOi0zkCtvAaBa8WSnDPtUN5RzmUtkKoMxBzu/MEMWNYXZyk/G2NHMJ
jh0N+ICpRNpnXGIZBwFymIRGH/PjtVkArVXy0hILWP/qfYzYMFgUBl0InyQzT0Hw
gzGdoeK7fVYrPWLK3ryRqqSR1XvfKFHwlnIg4XTBN4Cj7m5TmpntgVhO2JpNDjLr
K/NmtORHe6OhF17I
-----END CERTIFICATE-----

View file

@ -1,38 +0,0 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----

View file

@ -0,0 +1,41 @@
-----BEGIN CERTIFICATE-----
MIIHPDCCBSSgAwIBAgICEAcwDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAlVT
MQswCQYDVQQIDAJNQTEQMA4GA1UECgwHUmVkIEhhdDERMA8GA1UECwwIS2V5Y2xv
YWsxITAfBgNVBAMMGEtleWNsb2FrIEludGVybWVkaWF0ZSBDQTEjMCEGCSqGSIb3
DQEJARYUY29udGFjdEBrZXljbG9hay5vcmcwHhcNMTkwMzA4MTgyMjU5WhcNNDYw
NzI0MTgyMjU5WjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQH
DAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNVBAsMCEtleWNsb2FrMRIw
EAYDVQQDDAl0ZXN0LXVzZXIxIjAgBgkqhkiG9w0BCQEWE3Rlc3QtdXNlckBsb2Nh
bGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDpitg+FXUbxjlI
wD1l6Jef4ZDMAjSl4DtGa4E5ga8yJ/BfDv0AmL5DYEQEyASDdvpzSvj3o/erRx84
TwtOzuyjAy53I0hI45mdsZr4dhYz6/saKE/sdJs792vTIVQmI1hzO8fi1rgADJ3u
MT8deADFWWvj+2E5s2m2zFhzPYPSLcY8pf46ZLfS5lrGYdl77fejYD+AhtVXoJpd
JzZ0egCMCpSpdseTTLl64QrNsp9D60lcMx7HSGo6mkwxnncIVqS8wsv/5Nyi0/cn
UWoYW1CliuPAzy3/nCbm1RnBP4XYgEKgNQv91Jv5F0dT3CIxt2C3l2r4Zk/+x+d5
UXtZnR5lJ9W+1a+qGF+7pZ/MGagTL3Hjitt8JCmPe9I9jeOlIwAXMPX51HJCmII6
b/CNNvT4JyIAY1962cjJkQfCocPjHFSMdA7Bce6CXHOWVdekTOLR8ddOxdPODgZA
5KidJONqcNYKbKL5Z/j1ShnrQRhWwALDcDDGcZiU/69UVVpOLqXvx381s9T78HE4
2kQ/DM4QtesTq+x0fLg0QxVONPl+ZpBCZM70+fooe2uuE7EDWblPw8d4+Z3GKbSz
JdBb85TZXw5Gd1wlEH5K/aP58XavQ0wRqcupzGguQTH/Dys41wupYqFAUExSRqx7
HOfT0yNBkjl5JbP4DuPeEpmyJApmqwIDAQABo4IBrTCCAakwCQYDVR0TBAIwADAR
BglghkgBhvhCAQEEBAMCBaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJh
dGVkIENsaWVudCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU+mP2lV1sZIgt0Drjepyg
o2YEXW0wDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDAqBgNVHR8EIzAhMB+gHaAbhhlodHRwOi8vbG9jYWxob3N0Ojg4ODgvY3Js
MDYGCCsGAQUFBwEBBCowKDAmBggrBgEFBQcwAYYaaHR0cDovL2xvY2FsaG9zdDo4
ODg4L29zY3AwgaEGA1UdEQSBmTCBloEbdGVzdC11c2VyLWFsdG1haWxAbG9jYWxo
b3N0hwTAqAcBghR3d3cuZXhhbXBsZS10ZXN0LmNvbYYbaHR0cDovL3d3dy5leGFt
cGxlLXRlc3QuY29toBUGAyoDBKAODAxteV90ZXN0X3VzZXKgJwYKKwYBBAGCNxQC
A6AZDBd0ZXN0X3Vwbl9uYW1lQGxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
USFwAvT4dyxCP3Uqf+ztEWJx82L0rXuy9H+9nr1LC6AiHqyDzgtzwqF/clLmOJU6
JTFhNxf3fZUdHsLxNXpnpaZbYCkuo+Yh0FY3J3Qnhzht+csroqN/PWKmBV+dN8kq
SWw1327LHsX3C6ItnMUigUmMyYx+2WtNxCweacFczlwCpAx2cy+/eP4jX9tMWg8h
/AZs7XJL4zwqum7bSIsp2EkbeIqH60bqcMy6tFAb1+OwahHW8dSub4TQCpHPR5y7
0CJNQXUOSUTuQ51KndYqmoAL6xaQ0l1NCECZ2DGI6ja3HjjCXbxswv50i/0+xmUn
261IzBuWHQ56ub/fuTjLlC/O4QhSQZm0pd1zEtVlUg8+uApohyJgUSR2QO6iDWC8
zE5JjxVVg6h7ynEBtMQYkt0WXdfGQPMkUgHWaRl125GZHajsxxTfhbAHqVGITZ6z
eYYn8F3GM3Dp4ph+V0zRgaF37JoBT0x7xnDZXBXyzCa6w7/3/ijg6RMFwbVU//c8
htlcilkLcXOTS3C9+OThSLK8yBlQy0GYQqiWYWuMEPXY7QksaCM6A7P0M4+d8bvS
nbVsveIXho1bpiVOjobJJP+Lk88CUGvgaV4P+ksWdRKjzc2TGJo8k9kYTECVGaCp
z/X6dohZxgFWxLcZQ8q5HYqIcyH/qbBy4z14yerbOqs=
-----END CERTIFICATE-----

View file

@ -84,7 +84,8 @@ stateOrProvinceName_default = MA
localityName_default = Boston localityName_default = Boston
0.organizationName_default = Red Hat 0.organizationName_default = Red Hat
organizationalUnitName_default = Keycloak organizationalUnitName_default = Keycloak
emailAddress_default = contact@keycloak.org commonName_default = test-user
emailAddress_default = test-user@localhost
[ v3_ca ] [ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`). # Extensions for a typical CA (`man x509v3_config`).
@ -106,13 +107,21 @@ basicConstraints = CA:FALSE
nsCertType = client, email nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate" nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer #authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection extendedKeyUsage = clientAuth, emailProtection
crlDistributionPoints = URI:http://localhost:8888/crl crlDistributionPoints = URI:http://localhost:8888/crl
authorityInfoAccess = OCSP;URI:http://localhost:8888/oscp authorityInfoAccess = OCSP;URI:http://localhost:8888/oscp
subjectAltName=email:copy subjectAltName=@user_subject_alt_names
subjectAltName=email:move
[ user_subject_alt_names ]
email = test-user-altmail@localhost
IP = 192.168.7.1
DNS = www.example-test.com
URI = http://www.example-test.com
otherName.1 = 1.2.3.4;UTF8:my_test_user
otherName.2 = 1.3.6.1.4.1.311.20.2.3;UTF8:test_upn_name@localhost
[ server_cert ] [ server_cert ]
# Extensions for server certificates (`man x509v3_config`). # Extensions for server certificates (`man x509v3_config`).

View file

@ -195,7 +195,7 @@
<resource> <resource>
<directory>${common.resources}/pki/root/ca</directory> <directory>${common.resources}/pki/root/ca</directory>
<includes> <includes>
<include>certs/clients/test-user-san-email@localhost.cert.pem</include> <include>certs/clients/test-user-san@localhost.cert.pem</include>
<include>certs/clients/test-user@localhost.key.pem</include> <include>certs/clients/test-user@localhost.key.pem</include>
</includes> </includes>
</resource> </resource>

View file

@ -88,6 +88,12 @@
<resource> <resource>
<directory>${common.resources}/keystore</directory> <directory>${common.resources}/keystore</directory>
</resource> </resource>
<resource>
<directory>${common.resources}/pki/root/ca</directory>
<includes>
<include>certs/clients/*</include>
</includes>
</resource>
</resources> </resources>
</build> </build>

View file

@ -273,7 +273,7 @@
<outputDirectory>${containers.home}/auth-server-undertow</outputDirectory> <outputDirectory>${containers.home}/auth-server-undertow</outputDirectory>
</artifactItem> </artifactItem>
</artifactItems> </artifactItems>
<includes>*.jks,*.crt,*.truststore</includes> <includes>*.jks,*.crt,*.truststore,*.crl,*.key,certs/clients/*</includes>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>

View file

@ -49,7 +49,6 @@ 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;
import org.keycloak.testsuite.util.DroneUtils; import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.PhantomJSBrowser;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
@ -68,6 +67,7 @@ import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorC
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN; import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN;
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.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;
@ -117,26 +117,41 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED); Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED);
} }
@BeforeClass @BeforeClass
public static void onBeforeTestClass() { public static void onBeforeTestClass() {
if (isAuthServerJBoss()) { configurePhantomJS("/ca.crt", "/client.crt", "/client.key", "secret");
}
/**
* Setup phantom JS to be used for mutual TLS testing. All file paths are relative to "authServerHome"
*
* @param certificatesPath
* @param clientCertificateFile
* @param clientKeyFile
* @param clientKeyPassword
*/
protected static void configurePhantomJS(String certificatesPath, String clientCertificateFile, String clientKeyFile, String clientKeyPassword) {
String authServerHome = System.getProperty("auth.server.home"); String authServerHome = System.getProperty("auth.server.home");
if (authServerHome != null && System.getProperty("auth.server.ssl.required") != null) { if (authServerHome != null && System.getProperty("auth.server.ssl.required") != null) {
if (isAuthServerJBoss()) {
authServerHome = authServerHome + "/standalone/configuration"; authServerHome = authServerHome + "/standalone/configuration";
}
StringBuilder cliArgs = new StringBuilder(); StringBuilder cliArgs = new StringBuilder();
cliArgs.append("--ignore-ssl-errors=true "); cliArgs.append("--ignore-ssl-errors=true ");
cliArgs.append("--web-security=false "); cliArgs.append("--web-security=false ");
cliArgs.append("--ssl-certificates-path=").append(authServerHome).append("/ca.crt "); cliArgs.append("--ssl-certificates-path=").append(authServerHome).append(certificatesPath).append(" ");
cliArgs.append("--ssl-client-certificate-file=").append(authServerHome).append("/client.crt "); cliArgs.append("--ssl-client-certificate-file=").append(authServerHome).append(clientCertificateFile).append(" ");
cliArgs.append("--ssl-client-key-file=").append(authServerHome).append("/client.key "); cliArgs.append("--ssl-client-key-file=").append(authServerHome).append(clientKeyFile).append(" ");
cliArgs.append("--ssl-client-key-passphrase=secret "); cliArgs.append("--ssl-client-key-passphrase=" + clientKeyPassword).append(" ");
System.setProperty("keycloak.phantomjs.cli.args", cliArgs.toString()); System.setProperty("keycloak.phantomjs.cli.args", cliArgs.toString());
} }
} }
}
private static boolean isAuthServerJBoss() { private static boolean isAuthServerJBoss() {
return Boolean.parseBoolean(System.getProperty("auth.server.jboss")); return Boolean.parseBoolean(System.getProperty("auth.server.jboss"));
@ -183,6 +198,8 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
userId = user.getId(); userId = user.getId();
user.singleAttribute("x509_certificate_identity","-"); user.singleAttribute("x509_certificate_identity","-");
user.singleAttribute("alternative_email", "test-user-altmail@localhost");
user.singleAttribute("upn", "test_upn_name@localhost");
updateUser(user); updateUser(user);
} }
@ -343,11 +360,20 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe
.setUserIdentityMapperType(USERNAME_EMAIL); .setUserIdentityMapperType(USERNAME_EMAIL);
} }
protected static X509AuthenticatorConfigModel createLoginSubjectAltNameEmail2UsernameOrEmailConfig() { protected static X509AuthenticatorConfigModel createLoginSubjectAltNameEmail2UserAttributeConfig() {
return new X509AuthenticatorConfigModel() return new X509AuthenticatorConfigModel()
.setConfirmationPageAllowed(true) .setConfirmationPageAllowed(true)
.setMappingSourceType(SUBJECTALTNAME_EMAIL) .setMappingSourceType(SUBJECTALTNAME_EMAIL)
.setUserIdentityMapperType(USERNAME_EMAIL); .setUserIdentityMapperType(USER_ATTRIBUTE)
.setCustomAttributeName("alternative_email");
}
protected static X509AuthenticatorConfigModel createLoginSubjectAltNameOtherName2UserAttributeConfig() {
return new X509AuthenticatorConfigModel()
.setConfirmationPageAllowed(true)
.setMappingSourceType(SUBJECTALTNAME_OTHERNAME)
.setUserIdentityMapperType(USER_ATTRIBUTE)
.setCustomAttributeName("upn");
} }
protected static X509AuthenticatorConfigModel createLoginSubjectEmailWithKeyUsage(String keyUsage) { protected static X509AuthenticatorConfigModel createLoginSubjectEmailWithKeyUsage(String keyUsage) {

View file

@ -39,7 +39,7 @@ import org.openqa.selenium.WebDriver;
* @date 8/12/2016 * @date 8/12/2016
*/ */
public class X509BrowserLoginSubjectAltNameEmailTest extends AbstractX509AuthenticationTest { public class X509BrowserLoginSubjectAltNameTest extends AbstractX509AuthenticationTest {
@Page @Page
@PhantomJSBrowser @PhantomJSBrowser
@ -64,23 +64,8 @@ public class X509BrowserLoginSubjectAltNameEmailTest extends AbstractX509Authent
@BeforeClass @BeforeClass
public static void onBeforeTestClass() { public static void onBeforeTestClass() {
if (Boolean.parseBoolean(System.getProperty("auth.server.jboss"))) { configurePhantomJS("/ca.crt", "/certs/clients/test-user-san@localhost.cert.pem",
String authServerHome = System.getProperty("auth.server.home"); "/certs/clients/test-user@localhost.key.pem", "password");
if (authServerHome != null && System.getProperty("auth.server.ssl.required") != null) {
authServerHome = authServerHome + "/standalone/configuration";
StringBuilder cliArgs = new StringBuilder();
cliArgs.append("--ignore-ssl-errors=true ");
cliArgs.append("--web-security=false ");
cliArgs.append("--ssl-certificates-path=" + authServerHome + "/ca.crt ");
cliArgs.append("--ssl-client-certificate-file=" + authServerHome + "/certs/clients/test-user-san-email@localhost.cert.pem ");
cliArgs.append("--ssl-client-key-file=" + authServerHome + "/certs/clients/test-user@localhost.key.pem ");
cliArgs.append("--ssl-client-key-passphrase=password");
System.setProperty("keycloak.phantomjs.cli.args", cliArgs.toString());
}
}
} }
private void login(X509AuthenticatorConfigModel config, String userId, String username, String attemptedUsername) { private void login(X509AuthenticatorConfigModel config, String userId, String username, String attemptedUsername) {
@ -91,7 +76,7 @@ public class X509BrowserLoginSubjectAltNameEmailTest extends AbstractX509Authent
loginConfirmationPage.open(); loginConfirmationPage.open();
Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().equals("CN=test-user, OU=Keycloak, O=Red Hat, L=Boston, ST=MA, C=US")); Assert.assertEquals("EMAILADDRESS=test-user@localhost, CN=test-user, OU=Keycloak, O=Red Hat, L=Boston, ST=MA, C=US", loginConfirmationPage.getSubjectDistinguishedNameText());
Assert.assertEquals(username, loginConfirmationPage.getUsernameText()); Assert.assertEquals(username, loginConfirmationPage.getUsernameText());
loginConfirmationPage.confirm(); loginConfirmationPage.confirm();
@ -107,7 +92,12 @@ public class X509BrowserLoginSubjectAltNameEmailTest extends AbstractX509Authent
} }
@Test @Test
public void loginAsUserFromCertSubjectEmail() { public void loginAsUserFromCertSANEmail() {
login(createLoginSubjectAltNameEmail2UsernameOrEmailConfig(), userId, "test-user@localhost", "test-user@localhost"); login(createLoginSubjectAltNameEmail2UserAttributeConfig(), userId, "test-user@localhost", "test-user-altmail@localhost");
}
@Test
public void loginAsUserFromCertSANUpn() {
login(createLoginSubjectAltNameOtherName2UserAttributeConfig(), userId, "test-user@localhost", "test_upn_name@localhost");
} }
} }

View file

@ -39,9 +39,14 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.testsuite.util.cli.TestsuiteCLI; import org.keycloak.testsuite.util.cli.TestsuiteCLI;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.xnio.Options;
import org.xnio.SslClientAuthMode;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -380,7 +385,9 @@ public class KeycloakServer {
.setIoThreads(config.getWorkerThreads() / 8); .setIoThreads(config.getWorkerThreads() / 8);
if (config.getPortHttps() != -1) { if (config.getPortHttps() != -1) {
builder = builder.addHttpsListener(config.getPortHttps(), config.getHost(), createSSLContext()); builder = builder
.addHttpsListener(config.getPortHttps(), config.getHost(), createSSLContext())
.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, SslClientAuthMode.REQUESTED);
} }
server = new UndertowJaxrsServer(); server = new UndertowJaxrsServer();
@ -476,12 +483,29 @@ public class KeycloakServer {
} }
private SSLContext createSSLContext() throws Exception { private SSLContext createSSLContext() throws Exception {
KeyManager[] keyManagers = getKeyManagers();
if (keyManagers == null) {
return SSLContext.getDefault();
}
TrustManager[] trustManagers = getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
}
private KeyManager[] getKeyManagers() throws Exception {
String keyStorePath = System.getProperty("keycloak.tls.keystore.path"); String keyStorePath = System.getProperty("keycloak.tls.keystore.path");
if (keyStorePath == null) { if (keyStorePath == null) {
return SSLContext.getDefault(); return null;
} }
log.infof("Loading keystore from file: %s", keyStorePath);
InputStream stream = Files.newInputStream(Paths.get(keyStorePath)); InputStream stream = Files.newInputStream(Paths.get(keyStorePath));
if (stream == null) { if (stream == null) {
@ -490,20 +514,41 @@ public class KeycloakServer {
try (InputStream is = stream) { try (InputStream is = stream) {
KeyStore keyStore = KeyStore.getInstance("JKS"); KeyStore keyStore = KeyStore.getInstance("JKS");
char[] keyStorePassword = System.getProperty("keycloak.tls.keystore.password", "password").toCharArray(); char[] keyStorePassword = System.getProperty("keycloak.tls.keystore.password", "password").toCharArray();
keyStore.load(is, keyStorePassword); keyStore.load(is, keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword); keyManagerFactory.init(keyStore, keyStorePassword);
SSLContext sslContext = SSLContext.getInstance("TLS"); return keyManagerFactory.getKeyManagers();
}
}
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
return sslContext; private TrustManager[] getTrustManagers() throws Exception {
String trustStorePath = System.getProperty("keycloak.tls.truststore.path");
if (trustStorePath == null) {
return null;
}
log.infof("Loading truststore from file: %s", trustStorePath);
InputStream stream = Files.newInputStream(Paths.get(trustStorePath));
if (stream == null) {
throw new RuntimeException("Could not load truststore");
}
try (InputStream is = stream) {
KeyStore keyStore = KeyStore.getInstance("JKS");
char[] keyStorePassword = System.getProperty("keycloak.tls.truststore.password", "password").toCharArray();
keyStore.load(is, keyStorePassword);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
return trustManagerFactory.getTrustManagers();
} }
} }
} }

View file

@ -118,5 +118,39 @@
"enabled": true "enabled": true
} }
},
"login-protocol": {
"saml": {
"knownProtocols": [
"http=${auth.server.http.port}",
"https=${auth.server.https.port}"
]
}
},
"x509cert-lookup": {
"provider": "${keycloak.x509cert.lookup.provider:default}",
"default": {
"enabled": true
},
"haproxy": {
"enabled": true,
"sslClientCert": "x-ssl-client-cert",
"sslCertChainPrefix": "x-ssl-client-cert-chain",
"certificateChainLength": 1
},
"apache": {
"enabled": true,
"sslClientCert": "x-ssl-client-cert",
"sslCertChainPrefix": "x-ssl-client-cert-chain",
"certificateChainLength": 1
},
"nginx": {
"enabled": true,
"sslClientCert": "x-ssl-client-cert",
"sslCertChainPrefix": "x-ssl-client-cert-chain",
"certificateChainLength": 1
}
} }
} }