KEYCLOAK-18744 OpenBanking Brasil fix for X509 client authentication. More flexibility in Subject DN comparison.

This commit is contained in:
mposolda 2021-09-23 09:33:41 +02:00 committed by Marek Posolda
parent 3c00dba8ad
commit 5740e158e3
27 changed files with 542 additions and 25 deletions

View file

@ -5,11 +5,13 @@ import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.x509.X509ClientCertificateLookup;
import javax.security.auth.x500.X500Principal;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -17,11 +19,13 @@ import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -31,6 +35,23 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
public static final String ATTR_PREFIX = "x509";
public static final String ATTR_SUBJECT_DN = ATTR_PREFIX + ".subjectdn";
public static final String ATTR_ALLOW_REGEX_PATTERN_COMPARISON = ATTR_PREFIX + ".allow.regex.pattern.comparison";
// Custom OIDs defined in the OpenBanking Brasil - https://openbanking-brasil.github.io/specs-seguranca/open-banking-brasil-certificate-standards-1_ID1.html#name-client-certificate
// These are not recognized by default in RFC1779 or RFC2253 and hence not read in the java by default
private static final Map<String, String> CUSTOM_OIDS = new HashMap<>();
private static final Map<String, String> CUSTOM_OIDS_REVERSED = new HashMap<>();
static {
CUSTOM_OIDS.put("2.5.4.5", "serialNumber".toUpperCase());
CUSTOM_OIDS.put("2.5.4.15", "businessCategory".toUpperCase());
CUSTOM_OIDS.put("1.3.6.1.4.1.311.60.2.1.3", "jurisdictionCountryName".toUpperCase());
// Reverse map
for (Map.Entry<String, String> entry : CUSTOM_OIDS.entrySet()) {
CUSTOM_OIDS_REVERSED.put(entry.getValue(), entry.getKey());
}
}
protected static ServicesLogger logger = ServicesLogger.LOGGER;
@ -99,27 +120,42 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
return;
}
OIDCAdvancedConfigWrapper clientCfg = OIDCAdvancedConfigWrapper.fromClientModel(client);
String subjectDNRegexp = client.getAttribute(ATTR_SUBJECT_DN);
if (subjectDNRegexp == null || subjectDNRegexp.length() == 0) {
logger.errorf("[X509ClientCertificateAuthenticator:authenticate] " + ATTR_SUBJECT_DN + " is null or empty");
context.attempted();
return;
}
Pattern subjectDNPattern = Pattern.compile(subjectDNRegexp);
Optional<String> matchedCertificate = Arrays.stream(certs)
.map(certificate -> certificate.getSubjectDN().getName())
.filter(subjectdn -> subjectDNPattern.matcher(subjectdn).matches())
.findFirst();
Optional<String> matchedCertificate;
if (clientCfg.getAllowRegexPatternComparison()) {
Pattern subjectDNPattern = Pattern.compile(subjectDNRegexp);
matchedCertificate = Arrays.stream(certs)
.map(certificate -> certificate.getSubjectDN().getName())
.filter(subjectdn -> subjectDNPattern.matcher(subjectdn).matches())
.findFirst();
} else {
// OIDC/OAuth2 does not use regex comparison as it expects exact DN given in the format according to RFC4514. See RFC8705 for the details.
// We allow custom OIDs attributes to be "expanded" or not expanded in the given Subject DN
X500Principal expectedDNPrincipal = new X500Principal(subjectDNRegexp, CUSTOM_OIDS_REVERSED);
matchedCertificate = Arrays.stream(certs)
.filter(certificate -> expectedDNPrincipal.getName(X500Principal.RFC2253, CUSTOM_OIDS).equals(certificate.getSubjectX500Principal().getName(X500Principal.RFC2253, CUSTOM_OIDS)))
.map(certificate -> certificate.getSubjectDN().getName())
.findFirst();
}
if (!matchedCertificate.isPresent()) {
// We do quite expensive operation here, so better check the logging level beforehand.
if (logger.isDebugEnabled()) {
logger.debug("[X509ClientCertificateAuthenticator:authenticate] Couldn't match any certificate for pattern " + subjectDNRegexp);
logger.debug("[X509ClientCertificateAuthenticator:authenticate] Couldn't match any certificate for expected Subject DN '" + subjectDNRegexp + "' with allow regex pattern '" + clientCfg.getAllowRegexPatternComparison() + "'.");
logger.debug("[X509ClientCertificateAuthenticator:authenticate] Available SubjectDNs: " +
Arrays.stream(certs)
.map(cert -> cert.getSubjectDN().getName())
.collect(Collectors.toList()));
Arrays.stream(certs)
.map(cert -> cert.getSubjectDN().getName())
.collect(Collectors.toList()));
}
context.attempted();
return;
@ -179,4 +215,5 @@ public class X509ClientAuthenticator extends AbstractClientAuthenticator {
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -198,6 +198,16 @@ public class OIDCAdvancedConfigWrapper {
setAttribute(X509ClientAuthenticator.ATTR_SUBJECT_DN, tls_client_auth_subject_dn);
}
public boolean getAllowRegexPatternComparison() {
String attrVal = getAttribute(X509ClientAuthenticator.ATTR_ALLOW_REGEX_PATTERN_COMPARISON);
// Allow Regex Pattern Comparison by default due the backwards compatibility
return attrVal == null || Boolean.parseBoolean(attrVal);
}
public void setAllowRegexPatternComparison(boolean allowRegexPatternComparison) {
setAttribute(X509ClientAuthenticator.ATTR_ALLOW_REGEX_PATTERN_COMPARISON, String.valueOf(allowRegexPatternComparison));
}
public String getPkceCodeChallengeMethod() {
return getAttribute(OIDCConfigAttributes.PKCE_CODE_CHALLENGE_METHOD);
}

View file

@ -22,6 +22,7 @@ import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
@ -150,6 +151,9 @@ public class DescriptionConverter {
if (clientOIDC.getTlsClientAuthSubjectDn() != null) {
configWrapper.setTlsClientAuthSubjectDn(clientOIDC.getTlsClientAuthSubjectDn());
// According to specification, attribute tls_client_auth_subject_dn has subject DN in the exact expected format. There is no reason for support regex comparisons
configWrapper.setAllowRegexPatternComparison(false);
}
if (clientOIDC.getIdTokenSignedResponseAlg() != null) {

View file

@ -8,33 +8,37 @@ Passwords for any key file is `password`.
## Steps to create a client certificate
In the instructions below, you may usually need to create your own files for private key, CSR request, certificate , p12 and
also possibly custom openssl configuration. For the instructions below, replace the file names according your needs (For example
replace `test-user@localhost.key.pem` with something like `test-user-some@localhost.key.pem` )
### Create a private key for the client
openssl genrsa -aes256 -out signed/clients/test-user@localhost.key.pem 4096
chmod 400 signed/clients/test-user@localhost.key.pem
openssl genrsa -aes256 -out certs/clients/test-user@localhost.key.pem 4096
chmod 400 certs/clients/test-user@localhost.key.pem
### Create a CSR for the client
openssl req -config intermediate/openssl.cnf -key signed/clients/test-user@localhost.key.pem -new -sha256 -out signed/clients/test-user@localhost.csr.pem
openssl req -config intermediate/openssl.cnf -key certs/clients/test-user@localhost.key.pem -new -sha256 -out certs/clients/test-user@localhost.csr.pem
If you want to generate a CSR with extensions you can use a command similar to the following:
openssl req -config intermediate/openssl-san.cnf -key signed/clients/test-user@localhost.key.pem -new -sha256 -out signed/clients/test-user@localhost.csr.pem
openssl req -config intermediate/openssl-san.cnf -key certs/clients/test-user@localhost.key.pem -new -sha256 -out certs/clients/test-user@localhost.csr.pem
### Create a certificate using the CSR
openssl ca -config intermediate/openssl.cnf -extensions usr_cert -days 375 -notext -md sha256 -in signed/clients/test-user@localhost.csr.pem -out signed/clients/test-user@localhost.cert.pem
openssl ca -config intermediate/openssl.cnf -extensions usr_cert -days 375 -notext -md sha256 -in certs/clients/test-user@localhost.csr.pem -out certs/clients/test-user@localhost.cert.pem
chmod 444 signed/clients/test-user@localhost.cert.pem
chmod 444 certs/clients/test-user@localhost.cert.pem
### Verify the certificate
openssl x509 -noout -text -in signed/clients/test-user@localhost.cert.pem
openssl x509 -noout -text -in certs/clients/test-user@localhost.cert.pem
### Check if certificate has a valid chain of trust
openssl verify -CAfile intermediate/certs/ca-chain.cert.pem signed/clients/test-user@localhost.cert.pem
openssl verify -CAfile intermediate/certs/ca-chain.cert.pem certs/clients/test-user@localhost.cert.pem
### Transform both certificate and private key to PKCS12 format
openssl pkcs12 -export -in signed/clients/test-user@localhost.cert.pem -inkey signed/clients/test-user@localhost.key.pem -out signed/clients/test-user@localhost.p12 -name test-user -CAfile intermediate/certs/ca-chain.cert.pem
openssl pkcs12 -export -in certs/clients/test-user@localhost.cert.pem -inkey certs/clients/test-user@localhost.key.pem -out certs/clients/test-user@localhost.p12 -name test-user -CAfile intermediate/certs/ca-chain.cert.pem

View file

@ -0,0 +1,39 @@
-----BEGIN CERTIFICATE-----
MIIG5jCCBM6gAwIBAgICIAowDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAlVT
MQswCQYDVQQIDAJNQTEQMA4GA1UECgwHUmVkIEhhdDERMA8GA1UECwwIS2V5Y2xv
YWsxITAfBgNVBAMMGEtleWNsb2FrIEludGVybWVkaWF0ZSBDQTEjMCEGCSqGSIb3
DQEJARYUY29udGFjdEBrZXljbG9hay5vcmcwHhcNMjEwOTIyMTYxMTEyWhcNMzAw
MjIyMTYxMTEyWjCBnzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQH
DAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxFDASBgNVBAsMC015IE9yZyBVbml0
MQ0wCwYDVQQFEwQyMDA5MRgwFgYDVQQPDA9CdXNpbmVzcyBFbnRpdHkxEzARBgsr
BgEEAYI3PAIBAxMCQlIxDDAKBgNVBAMMA0ZvbzCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBALM0d3Ul6dPRqH62iPM1eYUO+Ldz6EhwfJRY4x2/3uTEu6YT
f6DK0dXyL1TOdiVFCGBbhz62nUXP1tkNTN4ue7f3mvaYPPq0Cy3Jat9zkf4CJl1m
K3Nm+8VjrXEG8wAci0kl8d2fSVN6ZdTBVcwiFBfyFAaQoIb5I8uDvMVBoxprNPuY
w44dStH/vAC60EyHG2jDoJ3648Oz9SZL33Xz1budrwrhEcsusDfg0sPeAMEsJPs+
FIWsnEWAQzjbqarxuTar2snUnVgb4MDWOsiL9IRZgN380rbV0jL8/4XFA7vicnJm
d/qsF9kP0KHi1oNvNkUOZdyOEwJoxb6KimXzflEIgkaPvYe+OXkwZkDvFUrcH7YN
W6FMLSpDGDRRd7ryJ0mpm1FwHOjLBE6Swhu+Uy9z3JF7EAJv8in0v98Gch6kmj0S
g38UEkdxTm5DmzkGpD8lWga5TfaijHjnGz8cPs1zEXarll0OTzQJwCTzWlc2wAzt
6vCq54iYZnVcENKgvZOonNelwVC1OwQT7FdYs6ZiczlLw/g9SXM51KiyK02T0GRi
QR6LfipDjXwxXlWQAS0QkuldKbQQF3P8kG4AM2H2WQtONCFo72FB1vqcX30Pyrxt
Q6x5B+r/P83qiwp3sjHGawIRNHrB5CUiEubXMePXtOYn596cmfeM5W1DRB2vAgMB
AAGjggFAMIIBPDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgB
hvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0G
A1UdDgQWBBQ7Y/YKQlK0DUoWdV5MFsmlMGuvATAOBgNVHQ8BAf8EBAMCBeAwHQYD
VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMGEGA1UdHwRaMFgwJaAjoCGGH2h0
dHA6Ly9sb2NhbGhvc3Q6ODg4OS9lbXB0eS5jcmwwL6AtoCuGKWh0dHA6Ly9sb2Nh
bGhvc3Q6ODg4OS9pbnRlcm1lZGlhdGUtY2EuY3JsMDYGCCsGAQUFBwEBBCowKDAm
BggrBgEFBQcwAYYaaHR0cDovL2xvY2FsaG9zdDo4ODg4L29zY3AwDQYJKoZIhvcN
AQELBQADggIBALmODoAsE4wNVvA8S0LnOcJGbk5bvt7kcoO0K76wA9QwOqstCLI9
UCDTUK0ZMGL8fQGBpGPKtPMFSbN8eWxm0jJHkXJkYbIdYW7uTQqQWfnYzCeERYhx
Pd1bB6bnAkBNELcl25AohhoalqOixoG7cegVs6Q4im6qEbGR18mh0l6Kw9FphePD
HSrBqUduJzuARCxvlOlyDMAP2d8caSrLziujJQGJ8Doz/fFpMI88MMjrelHoeNhy
d/HWcdjfb0f/URs742y9uuZMZoOaLeBJi5UPggmlvxpztonlmaJq11/9EwB5zWgm
rSyQzXUri3usXIpGxCtXPXIyn8nTlODyjbBUk3GD3S/n1voYyP1hFcm7mhke01Y3
D4DzquU7eFf/5cusAfgljcqe9xaHsAR7B/Ms9GQXs+/N6bASoWJl/WNeUFdBk+My
2CzYVyncpwb6vx9pFSaF81iNXCJn3B5+Lxl7H7lEdsrHgBx6qGT3MB/1vfEYfe7m
l/MZsEdaRgHvpQKlvtuYfgxQ71/vCCyd6w+QDIFOgfrlU15GlGubs9jnpiXE6/Xw
+3D4eTUOHPz3gYcFdUZoixC3INikih929fOXS3jXx4POlotjpq+B7lrWkILlc+DV
+2PaGwqdDoO6Lbkifjy80E3CABthjR2GdLgX3XwE0c+Yy0xPnNE6Iq2l
-----END CERTIFICATE-----

View file

@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIFGzCCAwMCAQAwgdUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNQTEPMA0GA1UE
BwwGQm9zdG9uMRAwDgYDVQQKDAdSZWQgSGF0MRQwEgYDVQQLDAtNeSBPcmcgVW5p
dDEMMAoGA1UEAwwDRm9vMQ0wCwYDVQQFEwQyMDA5MRgwFgYDVQQPDA9CdXNpbmVz
cyBFbnRpdHkxEzARBgsrBgEEAYI3PAIBAxMCQlIxNDAyBgoJkiaJk/IsZAEBDCRj
OTliMDJiZS1hN2Q1LTU5ODQtOTZjYi01Njc2ZDkxY2JmZWEwggIiMA0GCSqGSIb3
DQEBAQUAA4ICDwAwggIKAoICAQCzNHd1JenT0ah+tojzNXmFDvi3c+hIcHyUWOMd
v97kxLumE3+gytHV8i9UznYlRQhgW4c+tp1Fz9bZDUzeLnu395r2mDz6tAstyWrf
c5H+AiZdZitzZvvFY61xBvMAHItJJfHdn0lTemXUwVXMIhQX8hQGkKCG+SPLg7zF
QaMaazT7mMOOHUrR/7wAutBMhxtow6Cd+uPDs/UmS99189W7na8K4RHLLrA34NLD
3gDBLCT7PhSFrJxFgEM426mq8bk2q9rJ1J1YG+DA1jrIi/SEWYDd/NK21dIy/P+F
xQO74nJyZnf6rBfZD9Ch4taDbzZFDmXcjhMCaMW+iopl835RCIJGj72Hvjl5MGZA
7xVK3B+2DVuhTC0qQxg0UXe68idJqZtRcBzoywROksIbvlMvc9yRexACb/Ip9L/f
BnIepJo9EoN/FBJHcU5uQ5s5BqQ/JVoGuU32oox45xs/HD7NcxF2q5ZdDk80CcAk
81pXNsAM7erwqueImGZ1XBDSoL2TqJzXpcFQtTsEE+xXWLOmYnM5S8P4PUlzOdSo
sitNk9BkYkEei34qQ418MV5VkAEtEJLpXSm0EBdz/JBuADNh9lkLTjQhaO9hQdb6
nF99D8q8bUOseQfq/z/N6osKd7IxxmsCETR6weQlIhLm1zHj17TmJ+fenJn3jOVt
Q0QdrwIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBAAye7VRCBJP5VtZ5Sxf0fTUC
UZaOXn2e0JHpX3gfmQ007dZ6hhrP0bJwK/+K2WqgRwegzNErBIw/QkU+EmSOm5V3
ibN/+Kj4N7KsIKjyuaVjqxBTXk4+uleNazsAxLnGFeitNAm2u6lHScsxwS6Ahpgi
zgz9C5NqytFagsxfz9Bqm1ZWpBI9hHO0P3ch2HwX3f/bsEczPkSYghuyoLZp+XSO
mdW79LpPDC7Uiw04+dXTu8bBolB+Rl2XKM60PKLpEj6NS5OVymZxeUCPMgB05jiF
1c8tS1zfGKjXyhqsM4Cmvq9RtHFbMZ+6ZFZ62HKSgte9idUoBtZsvGJ4HrzSpD4R
EfCIZtweUMxfjhK5XycyELcr0TGZypayIU37Ntqsl4uspiuQkC2Yt4iXpxrSxLb2
qzeoWWlywrOG4gFe57Oo/ITZFwzjADb2fk9CwnpxW9M81YYfsLVAO0GEUxC5m13W
NNEhoIFGpUHJHBQVgIy73GnzL+kGFe48j2QZkECHJfZKwlxmPHZVxudMcLFKrRLT
47JJHo43NhOJFTxh0gesGef000WXRxz3k2dkgvU3rsm5i98pyi3whNhFpphspamf
qpr2OlfL0kT1IDOg8ASBUCDWTCTbsCviGTRO1Vdi4fmKnDc6iw0eJcg89/GjXS0E
QwCq8V8K9V83ofNQlCh/
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,48734C0265B7958D548AA8791226C442
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
-----END RSA PRIVATE KEY-----

View file

@ -1,2 +1,4 @@
V 450708200730Z 1000 unknown /C=US/ST=MA/L=Boston/O=Red Hat/OU=Keycloak/CN=test-user
R 460730105428Z 190314110930Z 1009 unknown /C=US/ST=MA/L=Westford/O=Red Hat/OU=Keycloak/CN=test-user@localhost/emailAddress=test-user@localhost
V 300222160951Z 2009 unknown /C=US/ST=MA/L=Boston/O=Red Hat/OU=My Org Unit/CN=Foo
V 300222161112Z 200A unknown /C=US/ST=MA/L=Boston/O=Red Hat/OU=My Org Unit/serialNumber=2009/businessCategory=Business Entity/jurisdictionC=BR/CN=Foo

View file

@ -1 +1,3 @@
V 450708200730Z 1000 unknown /C=US/ST=MA/L=Boston/O=Red Hat/OU=Keycloak/CN=test-user
R 460730105428Z 190314110930Z 1009 unknown /C=US/ST=MA/L=Westford/O=Red Hat/OU=Keycloak/CN=test-user@localhost/emailAddress=test-user@localhost
V 300222160951Z 2009 unknown /C=US/ST=MA/L=Boston/O=Red Hat/OU=My Org Unit/CN=Foo

View file

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

View file

@ -0,0 +1,39 @@
-----BEGIN CERTIFICATE-----
MIIG5jCCBM6gAwIBAgICIAowDQYJKoZIhvcNAQELBQAwgYcxCzAJBgNVBAYTAlVT
MQswCQYDVQQIDAJNQTEQMA4GA1UECgwHUmVkIEhhdDERMA8GA1UECwwIS2V5Y2xv
YWsxITAfBgNVBAMMGEtleWNsb2FrIEludGVybWVkaWF0ZSBDQTEjMCEGCSqGSIb3
DQEJARYUY29udGFjdEBrZXljbG9hay5vcmcwHhcNMjEwOTIyMTYxMTEyWhcNMzAw
MjIyMTYxMTEyWjCBnzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQH
DAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxFDASBgNVBAsMC015IE9yZyBVbml0
MQ0wCwYDVQQFEwQyMDA5MRgwFgYDVQQPDA9CdXNpbmVzcyBFbnRpdHkxEzARBgsr
BgEEAYI3PAIBAxMCQlIxDDAKBgNVBAMMA0ZvbzCCAiIwDQYJKoZIhvcNAQEBBQAD
ggIPADCCAgoCggIBALM0d3Ul6dPRqH62iPM1eYUO+Ldz6EhwfJRY4x2/3uTEu6YT
f6DK0dXyL1TOdiVFCGBbhz62nUXP1tkNTN4ue7f3mvaYPPq0Cy3Jat9zkf4CJl1m
K3Nm+8VjrXEG8wAci0kl8d2fSVN6ZdTBVcwiFBfyFAaQoIb5I8uDvMVBoxprNPuY
w44dStH/vAC60EyHG2jDoJ3648Oz9SZL33Xz1budrwrhEcsusDfg0sPeAMEsJPs+
FIWsnEWAQzjbqarxuTar2snUnVgb4MDWOsiL9IRZgN380rbV0jL8/4XFA7vicnJm
d/qsF9kP0KHi1oNvNkUOZdyOEwJoxb6KimXzflEIgkaPvYe+OXkwZkDvFUrcH7YN
W6FMLSpDGDRRd7ryJ0mpm1FwHOjLBE6Swhu+Uy9z3JF7EAJv8in0v98Gch6kmj0S
g38UEkdxTm5DmzkGpD8lWga5TfaijHjnGz8cPs1zEXarll0OTzQJwCTzWlc2wAzt
6vCq54iYZnVcENKgvZOonNelwVC1OwQT7FdYs6ZiczlLw/g9SXM51KiyK02T0GRi
QR6LfipDjXwxXlWQAS0QkuldKbQQF3P8kG4AM2H2WQtONCFo72FB1vqcX30Pyrxt
Q6x5B+r/P83qiwp3sjHGawIRNHrB5CUiEubXMePXtOYn596cmfeM5W1DRB2vAgMB
AAGjggFAMIIBPDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgB
hvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0G
A1UdDgQWBBQ7Y/YKQlK0DUoWdV5MFsmlMGuvATAOBgNVHQ8BAf8EBAMCBeAwHQYD
VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMGEGA1UdHwRaMFgwJaAjoCGGH2h0
dHA6Ly9sb2NhbGhvc3Q6ODg4OS9lbXB0eS5jcmwwL6AtoCuGKWh0dHA6Ly9sb2Nh
bGhvc3Q6ODg4OS9pbnRlcm1lZGlhdGUtY2EuY3JsMDYGCCsGAQUFBwEBBCowKDAm
BggrBgEFBQcwAYYaaHR0cDovL2xvY2FsaG9zdDo4ODg4L29zY3AwDQYJKoZIhvcN
AQELBQADggIBALmODoAsE4wNVvA8S0LnOcJGbk5bvt7kcoO0K76wA9QwOqstCLI9
UCDTUK0ZMGL8fQGBpGPKtPMFSbN8eWxm0jJHkXJkYbIdYW7uTQqQWfnYzCeERYhx
Pd1bB6bnAkBNELcl25AohhoalqOixoG7cegVs6Q4im6qEbGR18mh0l6Kw9FphePD
HSrBqUduJzuARCxvlOlyDMAP2d8caSrLziujJQGJ8Doz/fFpMI88MMjrelHoeNhy
d/HWcdjfb0f/URs742y9uuZMZoOaLeBJi5UPggmlvxpztonlmaJq11/9EwB5zWgm
rSyQzXUri3usXIpGxCtXPXIyn8nTlODyjbBUk3GD3S/n1voYyP1hFcm7mhke01Y3
D4DzquU7eFf/5cusAfgljcqe9xaHsAR7B/Ms9GQXs+/N6bASoWJl/WNeUFdBk+My
2CzYVyncpwb6vx9pFSaF81iNXCJn3B5+Lxl7H7lEdsrHgBx6qGT3MB/1vfEYfe7m
l/MZsEdaRgHvpQKlvtuYfgxQ71/vCCyd6w+QDIFOgfrlU15GlGubs9jnpiXE6/Xw
+3D4eTUOHPz3gYcFdUZoixC3INikih929fOXS3jXx4POlotjpq+B7lrWkILlc+DV
+2PaGwqdDoO6Lbkifjy80E3CABthjR2GdLgX3XwE0c+Yy0xPnNE6Iq2l
-----END CERTIFICATE-----

View file

@ -0,0 +1,151 @@
# OpenSSL intermediate CA configuration file.
[ ca ]
# `man ca`
default_ca = KeycloakICA
[ KeycloakICA ]
# Directory and file locations.
dir = ./intermediate
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/private/.rand
# The root key and root certificate.
private_key = $dir/private/intermediate.key.pem
certificate = $dir/certs/intermediate.cert.pem
# For certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate.crl.pem
crl_extensions = crl_ext
default_crl_days = 30
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose
[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
serialNumber = optional
businessCategory = optional
jurisdictionCountryName = optional
commonName = optional
[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
serialNumber = optional
businessCategory = optional
jurisdictionCountryName = optional
commonName = optional
[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
serialNumber = Serial Number
businessCategory = Business Category
jurisdictionCountryName = Jurisdiction Country Name
UID = User ID
# Optionally, specify some defaults.
countryName_default = US
stateOrProvinceName_default = MA
localityName_default = Boston
0.organizationName_default = Red Hat
organizationalUnitName_default = My Org Unit
commonName_default = Foo
serialNumber_default = 2009
businessCategory_default = Business Entity
jurisdictionCountryName_default = BR
UID_default = c99b02be-a7d5-5984-96cb-5676d91cbfea
[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ usr_cert ]
# Extensions for client certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
#authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
crlDistributionPoints=@crl_section
authorityInfoAccess = OCSP;URI:http://localhost:8888/oscp
[crl_section]
URI.1 = http://localhost:8889/empty.crl
URI.2 = http://localhost:8889/intermediate-ca.crl
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://localhost:8888/crl
authorityInfoAccess = OCSP;URI:http://localhost:8888/oscp
[ crl_ext ]
# Extension for CRLs (`man x509v3_config`).
authorityKeyIdentifier=keyid:always
[ ocsp ]
# Extension for OCSP signing certificates (`man ocsp`).
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning

View file

@ -213,8 +213,8 @@
<include>client.crt</include>
<include>client.key</include>
<include>*.crl</include>
<!-- KEYCLOAK-6771 Certificate Bound Token -->
<include>other_client.jks</include>
<include>test-user-obb.jks</include>
</includes>
</resource>
<resource>

View file

@ -105,8 +105,8 @@
<include>client.crt</include>
<include>client.key</include>
<include>*.crl</include>
<!-- KEYCLOAK-6771 Certificate Bound Token -->
<include>other_client.jks</include>
<include>test-user-obb.jks</include>
</includes>
</resource>
</resources>

View file

@ -428,8 +428,8 @@
<include>client.crt</include>
<include>client.key</include>
<include>*.crl</include>
<!-- KEYCLOAK-6771 Certificate Bound Token -->
<include>other_client.jks</include>
<include>test-user-obb.jks</include>
</includes>
</resource>
<resource>

View file

@ -34,6 +34,11 @@ public class MutualTLSUtils {
public static final String OTHER_KEYSTOREPATH = System.getProperty("hok.client.certificate.keystore");
public static final String OTHER_KEYSTOREPASSWORD = System.getProperty("hok.client.certificate.keystore.passphrase");
// Client certificate with tricky Subject, which contains RDNs with non-very known OID names
// like "jurisdictionCountryName", "businessCategory", "serialNumber" . These OIDs are used by OpenBanking Brasil
public static final String OBB_KEYSTOREPATH = System.getProperty("obb.client.certificate.keystore");
public static final String OBB_KEYSTOREPASSWORD = System.getProperty("obb.client.certificate.keystore.passphrase");
public static CloseableHttpClient newCloseableHttpClientWithDefaultKeyStoreAndTrustStore() {
return newCloseableHttpClient(DEFAULT_KEYSTOREPATH, DEFAULT_KEYSTOREPASSWORD, DEFAULT_TRUSTSTOREPATH, DEFAULT_TRUSTSTOREPASSWORD);
}
@ -41,6 +46,10 @@ public class MutualTLSUtils {
public static CloseableHttpClient newCloseableHttpClientWithOtherKeyStoreAndTrustStore() {
return newCloseableHttpClient(OTHER_KEYSTOREPATH, OTHER_KEYSTOREPASSWORD, DEFAULT_TRUSTSTOREPATH, DEFAULT_TRUSTSTOREPASSWORD);
}
public static CloseableHttpClient newCloseableHttpClientWithOBBKeyStoreAndTrustStore() {
return newCloseableHttpClient(OBB_KEYSTOREPATH, OBB_KEYSTOREPASSWORD, DEFAULT_TRUSTSTOREPATH, DEFAULT_TRUSTSTOREPASSWORD);
}
public static CloseableHttpClient newCloseableHttpClientWithoutKeyStoreAndTrustStore() {
return newCloseableHttpClient(null, null, null, null);

View file

@ -22,9 +22,11 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
@ -41,6 +43,7 @@ public class MutualTLSClientTest extends AbstractTestRealmKeycloakTest {
private static final String CLIENT_ID = "confidential-x509";
private static final String DISABLED_CLIENT_ID = "confidential-disabled-x509";
private static final String EXACT_SUBJECT_DN_CLIENT_ID = "confidential-subjectdn-x509";
private static final String OBB_SUBJECT_DN_CLIENT_ID = "obb-subjectdn-x509";
private static final String USER = "keycloak-user@localhost";
private static final String PASSWORD = "password";
private static final String REALM = "test";
@ -65,6 +68,12 @@ public class MutualTLSClientTest extends AbstractTestRealmKeycloakTest {
exactSubjectDNConfiguration.setRedirectUris(Arrays.asList("https://localhost:8543/auth/realms/master/app/auth"));
exactSubjectDNConfiguration.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
exactSubjectDNConfiguration.setAttributes(Collections.singletonMap(X509ClientAuthenticator.ATTR_SUBJECT_DN, EXACT_CERTIFICATE_SUBJECT_DN));
ClientRepresentation obbSubjectDNConfiguration = KeycloakModelUtils.createClient(testRealm, OBB_SUBJECT_DN_CLIENT_ID);
obbSubjectDNConfiguration.setServiceAccountsEnabled(Boolean.TRUE);
obbSubjectDNConfiguration.setRedirectUris(Arrays.asList("https://localhost:8543/auth/realms/master/app/auth"));
obbSubjectDNConfiguration.setClientAuthenticatorType(X509ClientAuthenticator.PROVIDER_ID);
// ATTR_SUBJECT_DN will be set in the individual tests based on the requested Subject DN Format
}
@BeforeClass
@ -149,6 +158,53 @@ public class MutualTLSClientTest extends AbstractTestRealmKeycloakTest {
assertTokenNotObtained(token);
}
@Test
public void testClientInvocationWithOBBClient_rfc2553_resolvedAttributes() throws Exception {
// Attributes like "JURISDICTIONCOUNTRYNAME", "BUSINESSCATEGORY" and SERIALNUMBER" are resolved (expanded) in the expected Subject DN
testClientInvocationWithOBBClient("CN=Foo,JURISDICTIONCOUNTRYNAME=BR,BUSINESSCATEGORY=Business Entity,SERIALNUMBER=2009,OU=My Org Unit,O=Red Hat,L=Boston,ST=MA,C=US",
true);
}
@Test
public void testClientInvocationWithOBBClient_rfc2553_unresolvedAttributes() throws Exception {
// Format like this is used by OpenBanking Brasil testsuite. Attributes like "JURISDICTIONCOUNTRYNAME", "BUSINESSCATEGORY" and SERIALNUMBER" are NOT resolved (expanded) in the expected Subject DN
testClientInvocationWithOBBClient("CN=Foo,1.3.6.1.4.1.311.60.2.1.3=#13024252,2.5.4.15=#130f427573696e65737320456e74697479,2.5.4.5=#130432303039,OU=My Org Unit,O=Red Hat,L=Boston,ST=MA,C=US",
true);
}
@Test
public void testClientInvocationWithOBBClient_rfc2553_invalidSubjectDN() throws Exception {
// Test that authentication fails when certificate does not contain expected DN
testClientInvocationWithOBBClient("CN=Foo,1.3.6.1.4.1.311.60.2.1.3=#13024252,2.5.4.15=#130f427573696e65737320456e74697479,2.5.4.5=#130e3037323337333733303030313230,OU=My Org Unit,O=Red Hat,L=Boston,ST=MA,C=US",
false);
}
@Test
public void testClientInvocationWithOBBClient_rfc1779() throws Exception {
testClientInvocationWithOBBClient("CN=Foo, JURISDICTIONCOUNTRYNAME=BR, BUSINESSCATEGORY=Business Entity, SERIALNUMBER=2009, OU=My Org Unit, O=Red Hat, L=Boston, ST=MA, C=US",
true);
}
private void testClientInvocationWithOBBClient(String expectedSubjectDN, boolean expectSuccess) throws Exception {
//given
Supplier<CloseableHttpClient> clientWithProperCertificate = MutualTLSUtils::newCloseableHttpClientWithOBBKeyStoreAndTrustStore;
// Canonical
ClientResource client = ApiUtil.findClientByClientId(testRealm(), OBB_SUBJECT_DN_CLIENT_ID);
ClientRepresentation clientRep = client.toRepresentation();
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
config.setAllowRegexPatternComparison(false);
config.setTlsClientAuthSubjectDn(expectedSubjectDN);
client.update(clientRep);
OAuthClient.AccessTokenResponse token = loginAndGetAccessTokenResponse(OBB_SUBJECT_DN_CLIENT_ID, clientWithProperCertificate);
if (expectSuccess) {
assertTokenObtained(token);
} else {
assertTokenNotObtained(token);
}
}
private OAuthClient.AccessTokenResponse loginAndGetAccessTokenResponse(String clientId, Supplier<CloseableHttpClient> client) throws IOException{
try (CloseableHttpClient closeableHttpClient = client.get()) {
login(clientId);

View file

@ -576,6 +576,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
Assert.assertEquals(X509ClientAuthenticator.PROVIDER_ID, kcClient.getClientAuthenticatorType());
Assert.assertEquals("Ein", config.getTlsClientAuthSubjectDn());
Assert.assertFalse(config.getAllowRegexPatternComparison());
// update
reg.auth(Auth.token(response));

View file

@ -155,8 +155,8 @@
<include>client.crt</include>
<include>client.key</include>
<include>*.crl</include>
<!-- KEYCLOAK-6771 Certificate Bound Token -->
<include>other_client.jks</include>
<include>test-user-obb.jks</include>
</includes>
</resource>
<resource>

View file

@ -252,6 +252,10 @@
<hok.client.certificate.keystore>${auth.server.config.dir}/other_client.jks</hok.client.certificate.keystore>
<hok.client.certificate.keystore.passphrase>secret</hok.client.certificate.keystore.passphrase>
<!-- Client certificate with the format suitable for OpenBanking Brasil -->
<obb.client.certificate.keystore>${auth.server.config.dir}/test-user-obb.jks</obb.client.certificate.keystore>
<obb.client.certificate.keystore.passphrase>password</obb.client.certificate.keystore.passphrase>
<auth.server.ocsp.responder.enabled>false</auth.server.ocsp.responder.enabled>
<keycloak.x509cert.lookup.provider>default</keycloak.x509cert.lookup.provider>
<auth.server.quarkus.cluster.config>local</auth.server.quarkus.cluster.config>
@ -642,6 +646,10 @@
<hok.client.certificate.keystore>${hok.client.certificate.keystore}</hok.client.certificate.keystore>
<hok.client.certificate.keystore.passphrase>${hok.client.certificate.keystore.passphrase}</hok.client.certificate.keystore.passphrase>
<!-- Client certificate with the format suitable for OpenBanking Brasil -->
<obb.client.certificate.keystore>${obb.client.certificate.keystore}</obb.client.certificate.keystore>
<obb.client.certificate.keystore.passphrase>${obb.client.certificate.keystore.passphrase}</obb.client.certificate.keystore.passphrase>
<auth.server.ocsp.responder.enabled>${auth.server.ocsp.responder.enabled}</auth.server.ocsp.responder.enabled>
<!-- cluster properties -->

View file

@ -1905,7 +1905,9 @@ request-uri-lifespan=Lifetime of the Request URI for Pushed Authorization Reques
request-uri-lifespan.tooltip=Number that represents the lifetime of the request URI in minutes or hours, the default value is 1 minute.
subjectdn=Subject DN
subjectdn-tooltip=A regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?:$)" to match all kind of expressions.
subjectdn-tooltip=The expected Subject DN, which should match DN from client certificate. In case that 'Allow Regex Pattern Comparison' allowed, this can contain regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?:$)" to match all kind of expressions.
allow-regex-pattern-comparison=Allow Regex Pattern Comparison
allow-regex-pattern-comparison.tooltip=If OFF, then the Subject DN from given client certificate must exactly match the given DN from the 'Subject DN' property as described in the RFC8705 specification. The Subject DN can be in the RFC2553 or RFC1779 format. If ON, then the Subject DN from given client certificate should match regex specified by 'Subject DN' property.
pkce-code-challenge-method=Proof Key for Code Exchange Code Challenge Method
pkce-code-challenge-method.tooltip=Choose which code challenge method for PKCE is used. If not specified, keycloak does not applies PKCE to a client unless the client sends an authorization request with appropriate code challenge and code exchange method.

View file

@ -198,7 +198,29 @@ module.controller('ClientX509Ctrl', function($scope, $location, Client, Notifica
}
}, true);
function updateProperties() {
if ($scope.client.attributes["x509.allow.regex.pattern.comparison"]) {
if ($scope.client.attributes["x509.allow.regex.pattern.comparison"] == "true") {
$scope.allowRegexPatternComparison = true;
} else {
$scope.allowRegexPatternComparison = false;
}
}
}
updateProperties();
$scope.switchChange = function() {
$scope.changed = true;
}
$scope.save = function() {
if ($scope.allowRegexPatternComparison == true) {
$scope.client.attributes["x509.allow.regex.pattern.comparison"] = "true";
} else {
$scope.client.attributes["x509.allow.regex.pattern.comparison"] = "false";
}
if (!$scope.client.attributes["x509.subjectdn"]) {
Notifications.error("The SubjectDN must not be empty.");
} else {
@ -224,6 +246,8 @@ module.controller('ClientX509Ctrl', function($scope, $location, Client, Notifica
$scope.reset = function() {
$scope.client.attributes["x509.subjectdn"] = $scope.clientCopy.attributes["x509.subjectdn"];
$scope.client.attributes["x509.allow.regex.pattern.comparison"] = $scope.clientCopy.attributes["x509.allow.regex.pattern.comparison"];
updateProperties();
$location.url("/realms/" + $scope.realm.realm + "/clients/" + $scope.client.id + "/credentials");
};
});

View file

@ -11,6 +11,13 @@
</div>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="allowRegexPatternComparison">{{:: 'allow-regex-pattern-comparison' | translate}}</label>
<div class="col-sm-6">
<input ng-model="allowRegexPatternComparison" name="allowRegexPatternComparison" id="allowRegexPatternComparison" ng-click="switchChange()" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'allow-regex-pattern-comparison.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="client.access.configure">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>