keycloak-uncached

Details

diff --git a/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java
index 83b1b6b..1e8b504 100644
--- a/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java
+++ b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java
@@ -40,7 +40,7 @@ public abstract class AbstractClientCertificateFromHttpHeadersLookup implements 
 
     protected final String sslClientCertHttpHeader;
     protected final String sslCertChainHttpHeaderPrefix;
-    private final int certificateChainLength;
+    protected final int certificateChainLength;
 
     public AbstractClientCertificateFromHttpHeadersLookup(String sslCientCertHttpHeader,
                                                           String sslCertChainHttpHeaderPrefix,
diff --git a/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java
new file mode 100644
index 0000000..7b11f5d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookup.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2017 Analytical Graphics, 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.services.x509;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
+import org.jboss.logging.Logger.Level;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.common.util.PemException;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.truststore.TruststoreProvider;
+import org.keycloak.truststore.TruststoreProviderFactory;
+
+/**
+ * The NGINX Provider extract end user X.509 certificate send during TLS mutual authentication,
+ * and forwarded in an http header.
+ * 
+ * NGINX configuration must have : 
+ * <code>
+ * server { 
+ *    ...
+ *    ssl_client_certificate                  path-to-my-trustyed-cas-for client auth.pem;
+ *    ssl_verify_client                       on|optional_no_ca;
+ *    ssl_verify_depth                        2;
+ *    ...
+ *    location / {
+ *    ...
+ *      sproxy_set_header ssl-client-cert        $ssl_client_escaped_cert;
+ *    ...
+ *  }
+ * </code>
+ * 
+ * Note that $ssl_client_cert is deprecated, use only $ssl_client_escaped_cert with this implementation
+ * 
+ * @author <a href="mailto:arnault.michel@toad-consulting.com">Arnault MICHEL</a>
+ * @version $Revision: 1 $
+ * @since 10/09/2018
+ */
+
+public class NginxProxySslClientCertificateLookup extends AbstractClientCertificateFromHttpHeadersLookup {
+
+	private static final Logger log = Logger.getLogger(NginxProxySslClientCertificateLookup.class);
+	
+	private static KeyStore  truststore = null;
+	private static Set<X509Certificate> trustedRootCerts = null;
+	private static Set<X509Certificate> intermediateCerts = null;
+	
+    
+    public NginxProxySslClientCertificateLookup(String sslCientCertHttpHeader,
+                                                 String sslCertChainHttpHeaderPrefix,
+                                                 int certificateChainLength,
+                                                 KeycloakSession kcsession) {
+        super(sslCientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
+
+    	if (!loadKeycloakTrustStore(kcsession)) {
+            log.warn("Keycloak Truststore is null or empty, but it's needed to rebuild client certificate chain with nginx Keycloak Provider");
+            log.warn("   see Keycloak documentation here : https://www.keycloak.org/docs/latest/server_installation/index.html#_truststore");
+    	}
+    	log.debug(" Keycloak truststore loaded for NGINX client certificate provider.");
+    }
+
+    /**
+     * Removing PEM Headers and end of lines
+     * @param pem
+     * @return
+     */
+    private static String removeBeginEnd(String pem) {
+        pem = pem.replace("-----BEGIN CERTIFICATE-----", "");
+        pem = pem.replace("-----END CERTIFICATE-----", "");
+        pem = pem.replace("\r\n", "");
+        pem = pem.replace("\n", "");
+        return pem.trim();
+    }
+
+    /**
+     * Decoding end user certificate, including URL decodeding due to ssl_client_escaped_cert nginx variable.
+     */
+    @Override
+    protected X509Certificate decodeCertificateFromPem(String pem) throws PemException {
+
+        if (pem == null) {
+        	log.info("End user TLS Certificate is NULL! ");
+            return null;
+        }
+    	try {
+			pem = java.net.URLDecoder.decode(pem, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			log.error("Cannot URL decode the end user TLS Certificate : " + pem,e);
+		}
+    	
+        if (pem.startsWith("-----BEGIN CERTIFICATE-----")) {
+            pem = removeBeginEnd(pem);
+        }
+
+        return PemUtils.decodeCertificate(pem);
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(HttpRequest httpRequest) throws GeneralSecurityException {
+        List<X509Certificate> chain = new ArrayList<>();
+
+        // Get the client certificate
+        X509Certificate clientCert = getCertificateFromHttpHeader(httpRequest, sslClientCertHttpHeader);
+        log.debugf("End user certificate found : DN=[%s]  SerialNumber=[%s]", clientCert.getSubjectDN().toString(), clientCert.getSerialNumber().toString() );
+        
+        if (clientCert != null) {
+            
+        	// Rebuilding the end user certificate chain using Keycloak Truststore
+            X509Certificate[] certChain = buildChain(clientCert);
+            for (X509Certificate cacert : certChain) {
+            	chain.add(cacert);
+            	log.debugf("Rebuilded user cert chain DN : %s", cacert.getSubjectDN().toString() );
+            }
+        }
+        return chain.toArray(new X509Certificate[0]);
+    }
+
+    /**
+     * As NGINX cannot actually  send the CA Chain in http header, 
+     * @param end_user_auth_cert
+     * @return
+     */
+	public X509Certificate[] buildChain(X509Certificate end_user_auth_cert) {
+		
+		String javasecuritydebugoriginalsettings = setJVMDebuggingForCertPathBuilder();
+		
+		X509Certificate[] user_cert_chain = null;
+		
+        try {
+        	
+        	// No truststore : no way!
+            if (truststore == null) {
+                log.warn("Keycloak Truststore is null, but it is required !");
+                log.warn("  see https://www.keycloak.org/docs/latest/server_installation/index.html#_truststore");
+                return null;
+            }
+
+            // Create the selector that specifies the starting certificate
+            X509CertSelector selector = new X509CertSelector();
+            selector.setCertificate(end_user_auth_cert);
+
+            // Create the trust anchors (set of root CA certificates)
+            Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
+            for (X509Certificate trustedRootCert : trustedRootCerts) {
+                trustAnchors.add(new TrustAnchor(trustedRootCert, null));
+            }
+            // Configure the PKIX certificate builder algorithm parameters
+            PKIXBuilderParameters pkixParams = new PKIXBuilderParameters( trustAnchors, selector);
+            
+            // Disable CRL checks, as it's possibly done after depending on Keycloak settings
+            pkixParams.setRevocationEnabled(false);
+            pkixParams.setExplicitPolicyRequired(false);
+            pkixParams.setAnyPolicyInhibited(false);
+            pkixParams.setPolicyQualifiersRejected(false);
+            pkixParams.setMaxPathLength(certificateChainLength);
+            
+            // Adding the list of intermediate certificates + end user certificate
+            intermediateCerts.add(end_user_auth_cert);
+            CollectionCertStoreParameters intermediateCA_userCert = new CollectionCertStoreParameters(intermediateCerts);
+            CertStore intermediateCertStore = CertStore.getInstance("Collection", intermediateCA_userCert, "BC");
+            pkixParams.addCertStore(intermediateCertStore);
+
+            // Build and verify the certification chain (revocation status excluded)
+            CertPathBuilder certPathBuilder = CertPathBuilder.getInstance("PKIX","BC");
+            CertPath certPath = certPathBuilder.build(pkixParams).getCertPath();
+            log.debug("Certification path building OK, and contains " + certPath.getCertificates().size() + " X509 Certificates");            
+            
+            //Remove end user certificate
+            intermediateCerts.remove(end_user_auth_cert);
+            
+            user_cert_chain = convertCertPathtoX509CertArray( certPath );
+            
+        } catch (NoSuchAlgorithmException e) {
+        	log.error(e.getLocalizedMessage(),e);
+        } catch (CertPathBuilderException e) {
+            if ( log.isEnabled(Level.TRACE) )
+            	log.debug(e.getLocalizedMessage(),e);
+            else
+            	log.warn(e.getLocalizedMessage());
+        } catch (InvalidAlgorithmParameterException e) {
+        	log.error(e.getLocalizedMessage(),e);
+        } catch (NoSuchProviderException e) {
+        	log.error(e.getLocalizedMessage(),e);
+		}
+        
+        //Reset java security debug property to original value
+        if (javasecuritydebugoriginalsettings!=null)
+        	System.setProperty("java.security.debug",javasecuritydebugoriginalsettings);
+        
+        //Remove end user certificate
+        intermediateCerts.remove(end_user_auth_cert);
+        
+        return null;
+	}
+	
+	/**
+	 * Add setting JVM system properties for helping debugging CertPathBuilder
+	 * only if the trace log level is enabled.
+	 * 
+	 * @return the original value of system property java.security.debug
+	 */
+	private String setJVMDebuggingForCertPathBuilder() {
+    	
+		String origjvmsecdebprop = null;
+    	if ( log.isEnabled(Level.TRACE) ) {
+    		origjvmsecdebprop =  System.getProperty("java.security.debug");
+    		if (origjvmsecdebprop.indexOf("certpath") == -1) {
+    			if (origjvmsecdebprop.length() == 0)
+    				System.setProperty("java.security.debug","certpath");
+    			else
+    				System.setProperty("java.security.debug",origjvmsecdebprop + ",certpath");
+    		}
+    		
+    	}
+    	return origjvmsecdebprop;
+    	
+	}
+
+	public X509Certificate[] convertCertPathtoX509CertArray( CertPath certPath ) {
+        
+		X509Certificate[] x509certchain = null;
+				
+		if (certPath!=null) {
+            List<X509Certificate> trustedX509Chain = new ArrayList<X509Certificate>();
+            for (Certificate certificate : certPath.getCertificates() )
+        	    if ( certificate instanceof X509Certificate )
+        		    trustedX509Chain.add((X509Certificate)certificate);
+            x509certchain = trustedX509Chain.toArray(new X509Certificate[0]);
+		}
+
+		return x509certchain;
+		
+	}
+	
+	public boolean loadKeycloakTrustStore(KeycloakSession kcsession) {
+
+		boolean isTSLoaded = false;
+		KeycloakSessionFactory factory = kcsession.getKeycloakSessionFactory();
+        TruststoreProviderFactory truststoreFactory = (TruststoreProviderFactory) factory.getProviderFactory(TruststoreProvider.class, "file");
+        
+        TruststoreProvider provider = truststoreFactory.create(kcsession);
+        if ( ! (provider != null && provider.getTruststore() == null ) ) {
+        	truststore = provider.getTruststore();
+        	readTruststore();
+        	
+        	isTSLoaded = true;
+        }
+
+		return isTSLoaded;
+	}
+
+	/**
+	 * Get all certificates from Keycloak Truststore, and classify them in two lists : root CAs and intermediates CAs
+	 */
+	private void readTruststore() {
+		
+    	//Reading truststore aliases & certificates
+    	Enumeration enumeration;
+    	trustedRootCerts  = new HashSet<X509Certificate>();
+    	intermediateCerts = new HashSet<X509Certificate>();
+		try {
+
+			enumeration = truststore.aliases();
+
+            while(enumeration.hasMoreElements()) {
+
+                String alias = (String)enumeration.nextElement();
+                Certificate certificate = truststore.getCertificate(alias);
+
+                if (certificate instanceof X509Certificate) {
+                	X509Certificate cax509cert = (X509Certificate) certificate;
+                	if (isSelfSigned(cax509cert)) {
+                        trustedRootCerts.add(cax509cert);
+                        log.debug("Adding certificate from trustore as trsusted root CA (alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() +")");
+                    } else {
+                        intermediateCerts.add(cax509cert);
+                        log.debug("Adding certificate from trustore as intermediate CA (alias : "+alias + " | Subject DN : " + ((X509Certificate) certificate).getSubjectDN() +")");
+                    }
+                } else
+                	log.warn("Skipping certificate in "+ alias + " because it's not an X509Certificate");
+                
+            }
+		} catch (KeyStoreException e) {
+			log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
+		} catch (CertificateException e) {
+			log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
+		} catch (NoSuchAlgorithmException e) {
+			log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
+		} catch (NoSuchProviderException e) {
+			log.error("Error while reading Keycloak truststore "+e.getMessage(),e);
+		}
+	}
+	
+	/**
+     * Checks whether given X.509 certificate is self-signed.
+     */
+    public boolean isSelfSigned(X509Certificate cert)
+            throws CertificateException, NoSuchAlgorithmException,
+            NoSuchProviderException {
+        try {
+            // Try to verify certificate signature with its own public key
+            PublicKey key = cert.getPublicKey();
+            cert.verify(key);
+        	log.trace("certificate " + cert.getSubjectDN() + " detected as root CA");
+            return true;
+        } catch (SignatureException sigEx) {
+            // Invalid signature --> not self-signed
+        	log.trace("certificate have a bad signature : " + sigEx.getMessage(),sigEx);
+        } catch (InvalidKeyException keyEx) {
+            // Invalid key --> not self-signed
+        	log.trace("certificate " + cert.getSubjectDN() + " detected as intermediate CA");
+        }
+        return false;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookupFactory.java
new file mode 100644
index 0000000..dc16776
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/NginxProxySslClientCertificateLookupFactory.java
@@ -0,0 +1,42 @@
+package org.keycloak.services.x509;
+/*
+ * Copyright 2017 Analytical Graphics, 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.
+ *
+ */
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:arnault.michel@toad-consulting.com">Arnault MICHEL</a>
+ * @version $Revision: 1 $
+ * @since 10/09/2018
+ */
+
+public class NginxProxySslClientCertificateLookupFactory extends AbstractClientCertificateFromHttpHeadersLookupFactory {
+
+    private final static String PROVIDER = "nginx";
+
+    @Override
+    public X509ClientCertificateLookup create(KeycloakSession session) {
+        return new NginxProxySslClientCertificateLookup(sslClientCertHttpHeader,
+                sslChainHttpHeaderPrefix, certificateChainLength, session);
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER;
+    }
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory
index 7aef09b..5940bcf 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory
@@ -19,3 +19,4 @@
 org.keycloak.services.x509.DefaultClientCertificateLookupFactory
 org.keycloak.services.x509.HaProxySslClientCertificateLookupFactory
 org.keycloak.services.x509.ApacheProxySslClientCertificateLookupFactory
+org.keycloak.services.x509.NginxProxySslClientCertificateLookupFactory
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 79ced07..a40cf39 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -174,6 +174,12 @@
             "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
         }
     }
 }