@@ -, +, @@ --- java/org/apache/catalina/realm/JNDIRealm.java | 218 +++++++++++++++++++-- .../apache/catalina/realm/LocalStrings.properties | 5 + 2 files changed, 207 insertions(+), 16 deletions(-) --- a/java/org/apache/catalina/realm/JNDIRealm.java +++ a/java/org/apache/catalina/realm/JNDIRealm.java @@ -17,11 +17,15 @@ package org.apache.catalina.realm; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; @@ -49,6 +53,14 @@ import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.StartTlsRequest; +import javax.naming.ldap.StartTlsResponse; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; import org.apache.catalina.LifecycleException; import org.ietf.jgss.GSSCredential; @@ -439,6 +451,45 @@ public class JNDIRealm extends RealmBase { */ protected String spnegoDelegationQop = "auth-conf"; + /** + * Whether to use TLS for connections + */ + private boolean useTls = false; + + private StartTlsResponse tls = null; + + /** + * The list of enabled cipher suites used for establishing tls connections. + * null means to use the default cipher suites. + */ + private String[] cipherSuites = null; + + /** + * Verifier for hostnames in a tls secured connection. null + * means to use the default verifier. + */ + private HostnameVerifier verifier = null; + + private enum Verifier { + IGNORE(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + private final HostnameVerifier verifier; + + public HostnameVerifier getVerifier() { + return this.verifier; + } + + private Verifier(HostnameVerifier verifier) { + this.verifier = verifier; + } + } + + private SSLSocketFactory sslSocketFactory = null; + // ------------------------------------------------------------- Properties /** @@ -1022,6 +1073,67 @@ public class JNDIRealm extends RealmBase { } + public boolean getUseTls() { + return useTls; + } + + public void setUseTls(boolean useTls) { + this.useTls = useTls; + } + + private String[] getCipherSuitesArray() { + return cipherSuites; + } + + public String[] getCipherSuites() { + return cipherSuites; + } + + public void setCipherSuites(String suites) { + if (suites == null || suites.trim().isEmpty()) { + containerLog.warn(sm.getString("jndiRealm.emptyCipherSuites")); + this.cipherSuites = null; + } else { + this.cipherSuites = suites.trim().split("\\s*,\\s*"); + containerLog.debug(sm.getString("jndiRealm.cipherSuites", + Arrays.asList(this.cipherSuites))); + } + } + + public void setHostVerifierClassname(String verifierClassname) { + if (Verifier.valueOf(verifierClassname) != null) { + this.verifier = Verifier.valueOf(verifierClassname).getVerifier(); + } + } + + public HostnameVerifier getHostnameVerifier() { + return this.verifier; + } + + public void setTlsProtocol(String protocol) { + try { + SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(null, null, null); + this.sslSocketFactory = sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + List allowedProtocols; + allowedProtocols = Arrays.asList(getSupportedTlsProtocols()); + throw new IllegalArgumentException( + sm.getString("jndiRealm.invalidTlsProtocol", protocol, + allowedProtocols), e); + } + } + + private String[] getSupportedTlsProtocols() { + try { + SSLContext sslContext = SSLContext.getDefault(); + sslContext.init(null, null, null); + return sslContext.getSupportedSSLParameters().getProtocols(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(sm.getString("jndiRealm.exception"), e); + } + } + // ---------------------------------------------------------- Realm Methods /** @@ -1933,6 +2045,14 @@ public class JNDIRealm extends RealmBase { if (context == null) return; + // Close tls startResponse if used + if (tls != null) { + try { + tls.close(); + } catch (IOException e) { + containerLog.error(sm.getString("jndiRealm.tlsClose"), e); + } + } // Close our opened connection try { if (containerLog.isDebugEnabled()) @@ -2125,7 +2245,7 @@ public class JNDIRealm extends RealmBase { try { // Ensure that we have a directory context available - context = new InitialDirContext(getDirectoryContextEnvironment()); + context = createDirContext(getDirectoryContextEnvironment()); } catch (Exception e) { @@ -2135,7 +2255,7 @@ public class JNDIRealm extends RealmBase { containerLog.info(sm.getString("jndiRealm.exception.retry"), e); // Try connecting to the alternate url. - context = new InitialDirContext(getDirectoryContextEnvironment()); + context = createDirContext(getDirectoryContextEnvironment()); } finally { @@ -2149,6 +2269,70 @@ public class JNDIRealm extends RealmBase { } + private DirContext createDirContext(Hashtable env) throws NamingException { + if (useTls) { + return createTlsDirContext(env); + } else { + return new InitialDirContext(env); + } + } + + /** + * Create a tls enabled LdapContext and set the StartTlsResponse tls + * instance variable. + * + * @param env + * Environment to use for context creation + * @return configured {@link LdapContext} + * @throws NamingException + * when something goes wrong while negotiating the connection + */ + private DirContext createTlsDirContext( + Hashtable env) throws NamingException { + Map savedEnv = new HashMap<>(); + for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, + Context.SECURITY_CREDENTIALS, Context.SECURITY_PRINCIPAL, + Context.SECURITY_PROTOCOL)) { + Object entry = env.remove(key); + if (entry != null) { + savedEnv.put(key, entry); + } + } + LdapContext result = null; + try { + result = new InitialLdapContext(env, null); + tls = (StartTlsResponse) result + .extendedOperation(new StartTlsRequest()); + if (verifier != null) { + tls.setHostnameVerifier(verifier); + } + if (getCipherSuites() != null) { + tls.setEnabledCipherSuites(getCipherSuitesArray()); + } + try { + SSLSession sslSession; + if (sslSocketFactory == null) { + sslSession = tls.negotiate(); + } else { + sslSession = tls.negotiate(sslSocketFactory); + } + containerLog.debug(sm.getString("jndiRealm.negotiatedTls", + sslSession.getPeerPrincipal().getName(), + sslSession.getCipherSuite(), sslSession.getProtocol())); + } catch (IOException e) { + throw new NamingException(e.getMessage()); + } + } finally { + if (result != null) { + for (Map.Entry savedEntry : savedEnv.entrySet()) { + result.addToEnvironment(savedEntry.getKey(), + savedEntry.getValue()); + } + } + } + return result; + } + /** * Create our directory context configuration. * @@ -2164,29 +2348,31 @@ public class JNDIRealm extends RealmBase { else if (containerLog.isDebugEnabled() && connectionAttempt > 0) containerLog.debug("Connecting to URL " + alternateURL); env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); - if (connectionName != null) - env.put(Context.SECURITY_PRINCIPAL, connectionName); - if (connectionPassword != null) - env.put(Context.SECURITY_CREDENTIALS, connectionPassword); + + putIfNotNull(env, Context.SECURITY_PRINCIPAL, connectionName); + putIfNotNull(env, Context.SECURITY_CREDENTIALS, connectionPassword); + if (connectionURL != null && connectionAttempt == 0) env.put(Context.PROVIDER_URL, connectionURL); else if (alternateURL != null && connectionAttempt > 0) env.put(Context.PROVIDER_URL, alternateURL); - if (authentication != null) - env.put(Context.SECURITY_AUTHENTICATION, authentication); - if (protocol != null) - env.put(Context.SECURITY_PROTOCOL, protocol); - if (referrals != null) - env.put(Context.REFERRAL, referrals); - if (derefAliases != null) - env.put(JNDIRealm.DEREF_ALIASES, derefAliases); - if (connectionTimeout != null) - env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout); + + putIfNotNull(env, Context.SECURITY_AUTHENTICATION, authentication); + putIfNotNull(env, Context.SECURITY_PROTOCOL, protocol); + putIfNotNull(env, Context.REFERRAL, referrals); + putIfNotNull(env, JNDIRealm.DEREF_ALIASES, derefAliases); + putIfNotNull(env, "com.sun.jndi.ldap.connect.timeout", connectionTimeout); return env; } + private void putIfNotNull(Hashtable env, String key, String value) { + if (value == null) { + return; + } + env.put(key, value); + } /** * Release our use of this connection so that it can be recycled. --- a/java/org/apache/catalina/realm/LocalStrings.properties +++ a/java/org/apache/catalina/realm/LocalStrings.properties @@ -40,10 +40,15 @@ jdbcRealm.open=Exception opening database connection jdbcRealm.open.invalidurl=Driver "{0}" does not support the url "{1}" jndiRealm.authenticateFailure=Username {0} NOT successfully authenticated jndiRealm.authenticateSuccess=Username {0} successfully authenticated +jndiRealm.emptyCipherSuites=Empty String for cipher suites given. Using default cipher suites. +jndiRealm.cipherSuites=Enable [{0}] as cipher suites for tls connection. jndiRealm.close=Exception closing directory server connection jndiRealm.exception=Exception performing authentication jndiRealm.exception.retry=Exception performing authentication. Retrying... +jndiRealm.negotiatedTls=Negotiated tls connection to "{0}" using cipher suite "{1}" and protocol "{2}" +jndiRealm.invalidTlsProtocol=Given protocol "{0}" is invalid. It has to be one of {1} jndiRealm.open=Exception opening directory server connection +jndiRealm.tlsClose=Exception closing tls response memoryRealm.authenticateFailure=Username {0} NOT successfully authenticated memoryRealm.authenticateSuccess=Username {0} successfully authenticated memoryRealm.loadExist=Memory database file {0} cannot be read --