@@ -, +, @@
---
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
--