/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.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; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.naming.AuthenticationException; import javax.naming.CommunicationException; import javax.naming.CompositeName; import javax.naming.Context; import javax.naming.InvalidNameException; import javax.naming.Name; import javax.naming.NameNotFoundException; import javax.naming.NameParser; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.PartialResultException; import javax.naming.ServiceUnavailableException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; 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; import org.ietf.jgss.GSSName; /** *
Implementation of Realm that works with a directory * server accessed via the Java Naming and Directory Interface (JNDI) APIs. * The following constraints are imposed on the data structure in the * underlying directory server:
*DirContext that is accessed
* via the connectionURL property.connectURL
* an attempt will be made to use the alternateURL if it
* exists.userPattern property.userPattern property is not
* specified, a unique element can be located by searching the directory
* context. In this case:
* userSearch pattern specifies the search filter
* after substitution of the username.userBase property can be set to the element that
* is the base of the subtree containing users. If not specified,
* the search base is the top-level context.userSubtree property can be set to
* true if you wish to search the entire subtree of the
* directory context. The default value of false
* requests a search of only the current level.userPassword property is not specified.userPassword
* property is specified, in which case:
* userPassword property.
* RealmBase.digest() method (using the standard digest
* support included in RealmBase).
* RealmBase.digest()) are equal to the retrieved value
* for the user password attribute.DirContext that is accessed via the
* connectionURL property. This element has the following
* characteristics:
* roleSearch
* property.roleSearch pattern optionally includes pattern
* replacements "{0}" for the distinguished name, and/or "{1}" for
* the username, and/or "{2}" the value of an attribute from the
* user's directory entry (the attribute is specified by the
* userRoleAttribute property), of the authenticated user
* for which roles will be retrieved.roleBase property can be set to the element that
* is the base of the search for matching roles. If not specified,
* the entire context will be searched.roleSubtree property can be set to
* true if you wish to search the entire subtree of the
* directory context. The default value of false
* requests a search of only the current level.roleName property) containing the name of the
* role represented by this element.userRoleName property.commonRole property to the
* name of this role. The role doesn't have to exist in the directory.roleNested to true.
* The default value is false, so role searches will not find
* nested roles.<security-role-ref> element in
* the web application deployment descriptor allows applications to refer
* to roles programmatically by names other than those used in the
* directory server itself.TODO - Support connection pooling (including message
* format objects) so that authenticate() does not have to be
* synchronized.
WARNING - There is a reported bug against the Netscape * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to * successfully authenticated a non-existing user. The * report is here: https://bz.apache.org/bugzilla/show_bug.cgi?id=11210 . * With luck, Netscape has updated their provider code and this is not an * issue.
* * @author John Holman * @author Craig R. McClanahan */ public class JNDIRealm extends RealmBase { // ----------------------------------------------------- Instance Variables /** * The type of authentication to use */ protected String authentication = null; /** * The connection username for the server we will contact. */ protected String connectionName = null; /** * The connection password for the server we will contact. */ protected String connectionPassword = null; /** * The connection URL for the server we will contact. */ protected String connectionURL = null; /** * The directory context linking us to our directory server. */ protected DirContext context = null; /** * The JNDI context factory used to acquire our InitialContext. By * default, assumes use of an LDAP server using the standard JNDI LDAP * provider. */ protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; /** * How aliases should be dereferenced during search operations. */ protected String derefAliases = null; /** * Constant that holds the name of the environment property for specifying * the manner in which aliases should be dereferenced. */ public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases"; /** * Descriptive information about this Realm implementation. * @deprecated This will be removed in Tomcat 9 onwards. */ @Deprecated protected static final String name = "JNDIRealm"; /** * The protocol that will be used in the communication with the * directory server. */ protected String protocol = null; /** * Should we ignore PartialResultExceptions when iterating over NamingEnumerations? * Microsoft Active Directory often returns referrals, which lead * to PartialResultExceptions. Unfortunately there's no stable way to detect, * if the Exceptions really come from an AD referral. * Set to true to ignore PartialResultExceptions. */ protected boolean adCompat = false; /** * How should we handle referrals? Microsoft Active Directory often returns * referrals. If you need to follow them set referrals to "follow". * Caution: if your DNS is not part of AD, the LDAP client lib might try * to resolve your domain name in DNS to find another LDAP server. */ protected String referrals = null; /** * The base element for user searches. */ protected String userBase = ""; /** * The message format used to search for a user, with "{0}" marking * the spot where the username goes. */ protected String userSearch = null; /** * When searching for users, should the search be performed as the user * currently being authenticated? If false, {@link #connectionName} and * {@link #connectionPassword} will be used if specified, else an anonymous * connection will be used. */ private boolean userSearchAsUser = false; /** * The MessageFormat object associated with the current *userSearch.
*/
protected MessageFormat userSearchFormat = null;
/**
* Should we search the entire subtree for matching users?
*/
protected boolean userSubtree = false;
/**
* The attribute name used to retrieve the user password.
*/
protected String userPassword = null;
/**
* The name of the attribute inside the users
* directory entry where the value will be
* taken to search for roles
* This attribute is not used during a nested search
*/
protected String userRoleAttribute = null;
/**
* A string of LDAP user patterns or paths, ":"-separated
* These will be used to form the distinguished name of a
* user, with "{0}" marking the spot where the specified username
* goes.
* This is similar to userPattern, but allows for multiple searches
* for a user.
*/
protected String[] userPatternArray = null;
/**
* The message format used to form the distinguished name of a
* user, with "{0}" marking the spot where the specified username
* goes.
*/
protected String userPattern = null;
/**
* An array of MessageFormat objects associated with the current
* userPatternArray.
*/
protected MessageFormat[] userPatternFormatArray = null;
/**
* The base element for role searches.
*/
protected String roleBase = "";
/**
* The MessageFormat object associated with the current
* roleBase.
*/
protected MessageFormat roleBaseFormat = null;
/**
* The MessageFormat object associated with the current
* roleSearch.
*/
protected MessageFormat roleFormat = null;
/**
* The name of an attribute in the user's entry containing
* roles for that user
*/
protected String userRoleName = null;
/**
* The name of the attribute containing roles held elsewhere
*/
protected String roleName = null;
/**
* The message format used to select roles for a user, with "{0}" marking
* the spot where the distinguished name of the user goes. The "{1}"
* and "{2}" are described in the Configuration Reference.
*/
protected String roleSearch = null;
/**
* Should we search the entire subtree for matching memberships?
*/
protected boolean roleSubtree = false;
/**
* Should we look for nested group in order to determine roles?
*/
protected boolean roleNested = false;
/**
* When searching for user roles, should the search be performed as the user
* currently being authenticated? If false, {@link #connectionName} and
* {@link #connectionPassword} will be used if specified, else an anonymous
* connection will be used.
*/
protected boolean roleSearchAsUser = false;
/**
* An alternate URL, to which, we should connect if connectionURL fails.
*/
protected String alternateURL;
/**
* The number of connection attempts. If greater than zero we use the
* alternate url.
*/
protected int connectionAttempt = 0;
/**
* Add this role to every authenticated user
*/
protected String commonRole = null;
/**
* The timeout, in milliseconds, to use when trying to create a connection
* to the directory. The default is 5000 (5 seconds).
*/
protected String connectionTimeout = "5000";
/**
* The timeout, in milliseconds, to use when trying to read from a connection
* to the directory. The default is 5000 (5 seconds).
*/
protected String readTimeout = "5000";
/**
* The sizeLimit (also known as the countLimit) to use when the realm is
* configured with {@link #userSearch}. Zero for no limit.
*/
protected long sizeLimit = 0;
/**
* The timeLimit (in milliseconds) to use when the realm is configured with
* {@link #userSearch}. Zero for no limit.
*/
protected int timeLimit = 0;
/**
* Should delegated credentials from the SPNEGO authenticator be used if
* available
*/
protected boolean useDelegatedCredential = true;
/**
* The QOP that should be used for the connection to the LDAP server after
* authentication. This value is used to set the
* javax.security.sasl.qop environment property for the LDAP
* connection.
*/
protected String spnegoDelegationQop = "auth-conf";
/**
* Whether to use TLS for connections
*/
private boolean useStartTls = 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[] cipherSuitesArray = null;
/**
* Verifier for hostnames in a StartTLS secured connection. null
* means to use the default verifier.
*/
private HostnameVerifier hostnameVerifier = null;
/**
* {@link SSLSocketFactory} to use when connection with StartTLS enabled.
*/
private SSLSocketFactory sslSocketFactory = null;
/**
* Name of the class of the {@link SSLSocketFactory}. null
* means to use the default factory.
*/
private String sslSocketFactoryClassName;
/**
* Comma separated list of cipher suites to use for StartTLS. If empty, the
* default suites are used.
*/
private String cipherSuites;
/**
* Name of the class of the {@link HostnameVerifier}. null
* means to use the default verifier.
*/
private String hostNameVerifierClassName;
/**
* The ssl Protocol which will be used by StartTLS.
*/
private String sslProtocol;
private boolean forceDnHexEscape = false;
// ------------------------------------------------------------- Properties
public boolean getForceDnHexEscape() {
return forceDnHexEscape;
}
public void setForceDnHexEscape(boolean forceDnHexEscape) {
this.forceDnHexEscape = forceDnHexEscape;
}
/**
* @return the type of authentication to use.
*/
public String getAuthentication() {
return authentication;
}
/**
* Set the type of authentication to use.
*
* @param authentication The authentication
*/
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
/**
* @return the connection username for this Realm.
*/
public String getConnectionName() {
return this.connectionName;
}
/**
* Set the connection username for this Realm.
*
* @param connectionName The new connection username
*/
public void setConnectionName(String connectionName) {
this.connectionName = connectionName;
}
/**
* @return the connection password for this Realm.
*/
public String getConnectionPassword() {
return this.connectionPassword;
}
/**
* Set the connection password for this Realm.
*
* @param connectionPassword The new connection password
*/
public void setConnectionPassword(String connectionPassword) {
this.connectionPassword = connectionPassword;
}
/**
* @return the connection URL for this Realm.
*/
public String getConnectionURL() {
return this.connectionURL;
}
/**
* Set the connection URL for this Realm.
*
* @param connectionURL The new connection URL
*/
public void setConnectionURL(String connectionURL) {
this.connectionURL = connectionURL;
}
/**
* @return the JNDI context factory for this Realm.
*/
public String getContextFactory() {
return this.contextFactory;
}
/**
* Set the JNDI context factory for this Realm.
*
* @param contextFactory The new context factory
*/
public void setContextFactory(String contextFactory) {
this.contextFactory = contextFactory;
}
/**
* @return the derefAliases setting to be used.
*/
public java.lang.String getDerefAliases() {
return derefAliases;
}
/**
* Set the value for derefAliases to be used when searching the directory.
*
* @param derefAliases New value of property derefAliases.
*/
public void setDerefAliases(java.lang.String derefAliases) {
this.derefAliases = derefAliases;
}
/**
* @return the protocol to be used.
*/
public String getProtocol() {
return protocol;
}
/**
* Set the protocol for this Realm.
*
* @param protocol The new protocol.
*/
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* @return the current settings for handling PartialResultExceptions
*/
public boolean getAdCompat () {
return adCompat;
}
/**
* How do we handle PartialResultExceptions?
* True: ignore all PartialResultExceptions.
* @param adCompat true to ignore partial results
*/
public void setAdCompat (boolean adCompat) {
this.adCompat = adCompat;
}
/**
* @return the current settings for handling JNDI referrals.
*/
public String getReferrals () {
return referrals;
}
/**
* How do we handle JNDI referrals? ignore, follow, or throw
* (see javax.naming.Context.REFERRAL for more information).
* @param referrals The referral handling
*/
public void setReferrals (String referrals) {
this.referrals = referrals;
}
/**
* @return the base element for user searches.
*/
public String getUserBase() {
return this.userBase;
}
/**
* Set the base element for user searches.
*
* @param userBase The new base element
*/
public void setUserBase(String userBase) {
this.userBase = userBase;
}
/**
* @return the message format pattern for selecting users in this Realm.
*/
public String getUserSearch() {
return this.userSearch;
}
/**
* Set the message format pattern for selecting users in this Realm.
*
* @param userSearch The new user search pattern
*/
public void setUserSearch(String userSearch) {
this.userSearch = userSearch;
if (userSearch == null)
userSearchFormat = null;
else
userSearchFormat = new MessageFormat(userSearch);
}
public boolean isUserSearchAsUser() {
return userSearchAsUser;
}
public void setUserSearchAsUser(boolean userSearchAsUser) {
this.userSearchAsUser = userSearchAsUser;
}
/**
* @return the "search subtree for users" flag.
*/
public boolean getUserSubtree() {
return this.userSubtree;
}
/**
* Set the "search subtree for users" flag.
*
* @param userSubtree The new search flag
*/
public void setUserSubtree(boolean userSubtree) {
this.userSubtree = userSubtree;
}
/**
* @return the user role name attribute name for this Realm.
*/
public String getUserRoleName() {
return userRoleName;
}
/**
* Set the user role name attribute name for this Realm.
*
* @param userRoleName The new userRole name attribute name
*/
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}
/**
* @return the base element for role searches.
*/
public String getRoleBase() {
return this.roleBase;
}
/**
* Set the base element for role searches.
*
* @param roleBase The new base element
*/
public void setRoleBase(String roleBase) {
this.roleBase = roleBase;
if (roleBase == null)
roleBaseFormat = null;
else
roleBaseFormat = new MessageFormat(roleBase);
}
/**
* @return the role name attribute name for this Realm.
*/
public String getRoleName() {
return this.roleName;
}
/**
* Set the role name attribute name for this Realm.
*
* @param roleName The new role name attribute name
*/
public void setRoleName(String roleName) {
this.roleName = roleName;
}
/**
* @return the message format pattern for selecting roles in this Realm.
*/
public String getRoleSearch() {
return this.roleSearch;
}
/**
* Set the message format pattern for selecting roles in this Realm.
*
* @param roleSearch The new role search pattern
*/
public void setRoleSearch(String roleSearch) {
this.roleSearch = roleSearch;
if (roleSearch == null)
roleFormat = null;
else
roleFormat = new MessageFormat(roleSearch);
}
public boolean isRoleSearchAsUser() {
return roleSearchAsUser;
}
public void setRoleSearchAsUser(boolean roleSearchAsUser) {
this.roleSearchAsUser = roleSearchAsUser;
}
/**
* @return the "search subtree for roles" flag.
*/
public boolean getRoleSubtree() {
return this.roleSubtree;
}
/**
* Set the "search subtree for roles" flag.
*
* @param roleSubtree The new search flag
*/
public void setRoleSubtree(boolean roleSubtree) {
this.roleSubtree = roleSubtree;
}
/**
* @return the "The nested group search flag" flag.
*/
public boolean getRoleNested() {
return this.roleNested;
}
/**
* Set the "search subtree for roles" flag.
*
* @param roleNested The nested group search flag
*/
public void setRoleNested(boolean roleNested) {
this.roleNested = roleNested;
}
/**
* @return the password attribute used to retrieve the user password.
*/
public String getUserPassword() {
return this.userPassword;
}
/**
* Set the password attribute used to retrieve the user password.
*
* @param userPassword The new password attribute
*/
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public String getUserRoleAttribute() {
return userRoleAttribute;
}
public void setUserRoleAttribute(String userRoleAttribute) {
this.userRoleAttribute = userRoleAttribute;
}
/**
* @return the message format pattern for selecting users in this Realm.
*/
public String getUserPattern() {
return this.userPattern;
}
/**
* Set the message format pattern for selecting users in this Realm.
* This may be one simple pattern, or multiple patterns to be tried,
* separated by parentheses. (for example, either "cn={0}", or
* "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
* but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
* also valid. Complex search strings with &, etc are NOT supported.
*
* @param userPattern The new user pattern
*/
public void setUserPattern(String userPattern) {
this.userPattern = userPattern;
if (userPattern == null)
userPatternArray = null;
else {
userPatternArray = parseUserPatternString(userPattern);
int len = this.userPatternArray.length;
userPatternFormatArray = new MessageFormat[len];
for (int i=0; i < len; i++) {
userPatternFormatArray[i] =
new MessageFormat(userPatternArray[i]);
}
}
}
/**
* Getter for property alternateURL.
*
* @return Value of property alternateURL.
*/
public String getAlternateURL() {
return this.alternateURL;
}
/**
* Setter for property alternateURL.
*
* @param alternateURL New value of property alternateURL.
*/
public void setAlternateURL(String alternateURL) {
this.alternateURL = alternateURL;
}
/**
* @return the common role
*/
public String getCommonRole() {
return commonRole;
}
/**
* Set the common role
*
* @param commonRole The common role
*/
public void setCommonRole(String commonRole) {
this.commonRole = commonRole;
}
/**
* @return the connection timeout.
*/
public String getConnectionTimeout() {
return connectionTimeout;
}
/**
* Set the connection timeout.
*
* @param timeout The new connection timeout
*/
public void setConnectionTimeout(String timeout) {
this.connectionTimeout = timeout;
}
/**
* @return the read timeout.
*/
public String getReadTimeout() {
return readTimeout;
}
/**
* Set the read timeout.
*
* @param timeout The new read timeout
*/
public void setReadTimeout(String timeout) {
this.readTimeout = timeout;
}
public long getSizeLimit() {
return sizeLimit;
}
public void setSizeLimit(long sizeLimit) {
this.sizeLimit = sizeLimit;
}
public int getTimeLimit() {
return timeLimit;
}
public void setTimeLimit(int timeLimit) {
this.timeLimit = timeLimit;
}
public boolean isUseDelegatedCredential() {
return useDelegatedCredential;
}
public void setUseDelegatedCredential(boolean useDelegatedCredential) {
this.useDelegatedCredential = useDelegatedCredential;
}
public String getSpnegoDelegationQop() {
return spnegoDelegationQop;
}
public void setSpnegoDelegationQop(String spnegoDelegationQop) {
this.spnegoDelegationQop = spnegoDelegationQop;
}
/**
* @return flag whether to use StartTLS for connections to the ldap server
*/
public boolean getUseStartTls() {
return useStartTls;
}
/**
* Flag whether StartTLS should be used when connecting to the ldap server
*
* @param useStartTls
* {@code true} when StartTLS should be used. Default is
* {@code false}.
*/
public void setUseStartTls(boolean useStartTls) {
this.useStartTls = useStartTls;
}
/**
* @return list of the allowed cipher suites when connections are made using
* StartTLS
*/
private String[] getCipherSuitesArray() {
if (cipherSuites == null || cipherSuitesArray != null) {
return cipherSuitesArray;
}
if (this.cipherSuites.trim().isEmpty()) {
containerLog.warn(sm.getString("jndiRealm.emptyCipherSuites"));
this.cipherSuitesArray = null;
} else {
this.cipherSuitesArray = cipherSuites.trim().split("\\s*,\\s*");
containerLog.debug(sm.getString("jndiRealm.cipherSuites",
Arrays.toString(this.cipherSuitesArray)));
}
return this.cipherSuitesArray;
}
/**
* Set the allowed cipher suites when opening a connection using StartTLS.
* The cipher suites are expected as a comma separated list.
*
* @param suites
* comma separated list of allowed cipher suites
*/
public void setCipherSuites(String suites) {
this.cipherSuites = suites;
}
/**
* @return name of the {@link HostnameVerifier} class used for connections
* using StartTLS, or the empty string, if the default verifier
* should be used.
*/
public String getHostnameVerifierClassName() {
if (this.hostnameVerifier == null) {
return "";
}
return this.hostnameVerifier.getClass().getCanonicalName();
}
/**
* Set the {@link HostnameVerifier} to be used when opening connections
* using StartTLS. An instance of the given class name will be constructed
* using the default constructor.
*
* @param verifierClassName
* class name of the {@link HostnameVerifier} to be constructed
*/
public void setHostnameVerifierClassName(String verifierClassName) {
if (verifierClassName != null) {
this.hostNameVerifierClassName = verifierClassName.trim();
} else {
this.hostNameVerifierClassName = null;
}
}
/**
* @return the {@link HostnameVerifier} to use for peer certificate
* verification when opening connections using StartTLS.
*/
public HostnameVerifier getHostnameVerifier() {
if (this.hostnameVerifier != null) {
return this.hostnameVerifier;
}
if (this.hostNameVerifierClassName == null
|| hostNameVerifierClassName.equals("")) {
return null;
}
try {
Object o = constructInstance(hostNameVerifierClassName);
if (o instanceof HostnameVerifier) {
this.hostnameVerifier = (HostnameVerifier) o;
return this.hostnameVerifier;
} else {
throw new IllegalArgumentException(sm.getString(
"jndiRealm.invalidHostnameVerifier",
hostNameVerifierClassName));
}
} catch (ReflectiveOperationException | SecurityException e) {
throw new IllegalArgumentException(sm.getString(
"jndiRealm.invalidHostnameVerifier",
hostNameVerifierClassName), e);
}
}
/**
* Set the {@link SSLSocketFactory} to be used when opening connections
* using StartTLS. An instance of the factory with the given name will be
* created using the default constructor. The SSLSocketFactory can also be
* set using {@link JNDIRealm#setSslProtocol(String) setSslProtocol(String)}.
*
* @param factoryClassName
* class name of the factory to be constructed
*/
public void setSslSocketFactoryClassName(String factoryClassName) {
this.sslSocketFactoryClassName = factoryClassName;
}
/**
* Set the ssl protocol to be used for connections using StartTLS.
*
* @param protocol
* one of the allowed ssl protocol names
*/
public void setSslProtocol(String protocol) {
this.sslProtocol = protocol;
}
/**
* @return the list of supported ssl protocols by the default
* {@link SSLContext}
*/
private String[] getSupportedSslProtocols() {
try {
SSLContext sslContext = SSLContext.getDefault();
return sslContext.getSupportedSSLParameters().getProtocols();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(sm.getString("jndiRealm.exception"), e);
}
}
private Object constructInstance(String className)
throws ReflectiveOperationException {
Class> clazz = Class.forName(className);
return clazz.getConstructor().newInstance();
}
// ---------------------------------------------------------- Realm Methods
/**
* Return the Principal associated with the specified username and
* credentials, if there is one; otherwise return null.
*
* If there are any errors with the JDBC connection, executing
* the query or anything we return null (don't authenticate). This
* event is also logged, and the connection will be closed so that
* a subsequent request will automatically re-open it.
*
* @param username Username of the Principal to look up
* @param credentials Password or other credentials to use in
* authenticating this username
* @return the associated principal, or null if there is none.
*/
@Override
public Principal authenticate(String username, String credentials) {
DirContext context = null;
Principal principal = null;
try {
// Ensure that we have a directory context available
context = open();
// Occasionally the directory context will timeout. Try one more
// time before giving up.
try {
// Authenticate the specified username if possible
principal = authenticate(context, username, credentials);
} catch (NullPointerException | NamingException e) {
/*
* BZ 61313
* NamingException may or may not indicate an error that is
* recoverable via fail over. Therefore a decision needs to be
* made whether to fail over or not. Generally, attempting to
* fail over when it is not appropriate is better than not
* failing over when it is appropriate so the code always
* attempts to fail over for NamingExceptions.
*/
/*
* BZ 42449
* Catch NPE - Kludge Sun's LDAP provider with broken SSL.
*/
// log the exception so we know it's there.
containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = authenticate(context, username, credentials);
}
// Release this context
release(context);
// Return the authenticated Principal (if any)
return principal;
} catch (NamingException e) {
// Log the problem for posterity
containerLog.error(sm.getString("jndiRealm.exception"), e);
// Close the connection so that it gets reopened next time
if (context != null)
close(context);
// Return "not authenticated" for this request
if (containerLog.isDebugEnabled())
containerLog.debug("Returning null principal.");
return null;
}
}
// -------------------------------------------------------- Package Methods
// ------------------------------------------------------ Protected Methods
/**
* Return the Principal associated with the specified username and
* credentials, if there is one; otherwise return null.
*
* @param context The directory context
* @param username Username of the Principal to look up
* @param credentials Password or other credentials to use in
* authenticating this username
* @return the associated principal, or null if there is none.
*
* @exception NamingException if a directory server error occurs
*/
public synchronized Principal authenticate(DirContext context,
String username,
String credentials)
throws NamingException {
if (username == null || username.equals("")
|| credentials == null || credentials.equals("")) {
if (containerLog.isDebugEnabled())
containerLog.debug("username null or empty: returning null principal.");
return null;
}
if (userPatternArray != null) {
for (int curUserPattern = 0;
curUserPattern < userPatternFormatArray.length;
curUserPattern++) {
// Retrieve user information
User user = getUser(context, username, credentials, curUserPattern);
if (user != null) {
try {
// Check the user's credentials
if (checkCredentials(context, user, credentials)) {
// Search for additional roles
Listnull.
*
* @param context The directory context
* @param username Username to be looked up
* @return the User object
* @exception NamingException if a directory server error occurs
*
* @see #getUser(DirContext, String, String, int)
*/
protected User getUser(DirContext context, String username)
throws NamingException {
return getUser(context, username, null, -1);
}
/**
* Return a User object containing information about the user
* with the specified username, if found in the directory;
* otherwise return null.
*
* @param context The directory context
* @param username Username to be looked up
* @param credentials User credentials (optional)
* @return the User object
* @exception NamingException if a directory server error occurs
*
* @see #getUser(DirContext, String, String, int)
*/
protected User getUser(DirContext context, String username, String credentials)
throws NamingException {
return getUser(context, username, credentials, -1);
}
/**
* Return a User object containing information about the user
* with the specified username, if found in the directory;
* otherwise return null.
*
* If the userPassword configuration attribute is
* specified, the value of that attribute is retrieved from the
* user's directory entry. If the userRoleName
* configuration attribute is specified, all values of that
* attribute are retrieved from the directory entry.
*
* @param context The directory context
* @param username Username to be looked up
* @param credentials User credentials (optional)
* @param curUserPattern Index into userPatternFormatArray
* @return the User object
* @exception NamingException if a directory server error occurs
*/
protected User getUser(DirContext context, String username,
String credentials, int curUserPattern)
throws NamingException {
User user = null;
// Get attributes to retrieve from user entry
ArrayListnull.
*
* @param context The directory context
* @param username The username
* @param attrIds String[]containing names of attributes to
* @param dn Distinguished name of the user
* retrieve.
* @return the User object
* @exception NamingException if a directory server error occurs
*/
protected User getUserByPattern(DirContext context,
String username,
String[] attrIds,
String dn)
throws NamingException {
// If no attributes are requested, no need to look for them
if (attrIds == null || attrIds.length == 0) {
return new User(username, dn, null, null,null);
}
// Get required attributes from user entry
Attributes attrs = null;
try {
attrs = context.getAttributes(dn, attrIds);
} catch (NameNotFoundException e) {
return null;
}
if (attrs == null)
return null;
// Retrieve value of userPassword
String password = null;
if (userPassword != null)
password = getAttributeValue(userPassword, attrs);
String userRoleAttrValue = null;
if (userRoleAttribute != null) {
userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs);
}
// Retrieve values of userRoleName attribute
ArrayListUserPattern configuration attribute to
* locate the directory entry for the user with the specified
* username and return a User object; otherwise return
* null.
*
* @param context The directory context
* @param username The username
* @param credentials User credentials (optional)
* @param attrIds String[]containing names of attributes to
* @param curUserPattern Index into userPatternFormatArray
* @return the User object
* @exception NamingException if a directory server error occurs
* @see #getUserByPattern(DirContext, String, String[], String)
*/
protected User getUserByPattern(DirContext context,
String username,
String credentials,
String[] attrIds,
int curUserPattern)
throws NamingException {
User user = null;
if (username == null || userPatternFormatArray[curUserPattern] == null)
return null;
// Form the dn from the user pattern
String dn = userPatternFormatArray[curUserPattern].format(new String[] { username });
try {
user = getUserByPattern(context, username, attrIds, dn);
} catch (NameNotFoundException e) {
return null;
} catch (NamingException e) {
// If the getUserByPattern() call fails, try it again with the
// credentials of the user that we're searching for
try {
userCredentialsAdd(context, dn, credentials);
user = getUserByPattern(context, username, attrIds, dn);
} finally {
userCredentialsRemove(context);
}
}
return user;
}
/**
* Search the directory to return a User object containing
* information about the user with the specified username, if
* found in the directory; otherwise return null.
*
* @param context The directory context
* @param username The username
* @param attrIds String[]containing names of attributes to retrieve.
* @return the User object
* @exception NamingException if a directory server error occurs
*/
protected User getUserBySearch(DirContext context,
String username,
String[] attrIds)
throws NamingException {
if (username == null || userSearchFormat == null)
return null;
// Form the search filter
String filter = userSearchFormat.format(new String[] { username });
// Set up the search controls
SearchControls constraints = new SearchControls();
if (userSubtree) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
constraints.setCountLimit(sizeLimit);
constraints.setTimeLimit(timeLimit);
// Specify the attributes to be retrieved
if (attrIds == null)
attrIds = new String[0];
constraints.setReturningAttributes(attrIds);
NamingEnumerationuserPassword
* configuration attribute is specified, the credentials
* previously retrieved from the directory are compared explicitly
* with those presented by the user. Otherwise the presented
* credentials are checked by binding to the directory as the
* user.
*
* @param context The directory context
* @param user The User to be authenticated
* @param credentials The credentials presented by the user
* @return true if the credentials are validated
* @exception NamingException if a directory server error occurs
*/
protected boolean checkCredentials(DirContext context,
User user,
String credentials)
throws NamingException {
boolean validated = false;
if (userPassword == null) {
validated = bindAsUser(context, user, credentials);
} else {
validated = compareCredentials(context, user, credentials);
}
if (containerLog.isTraceEnabled()) {
if (validated) {
containerLog.trace(sm.getString("jndiRealm.authenticateSuccess",
user.getUserName()));
} else {
containerLog.trace(sm.getString("jndiRealm.authenticateFailure",
user.getUserName()));
}
}
return validated;
}
/**
* Check whether the credentials presented by the user match those
* retrieved from the directory.
*
* @param context The directory context
* @param info The User to be authenticated
* @param credentials Authentication credentials
* @return true if the credentials are validated
* @exception NamingException if a directory server error occurs
*/
protected boolean compareCredentials(DirContext context,
User info,
String credentials)
throws NamingException {
// Validate the credentials specified by the user
if (containerLog.isTraceEnabled())
containerLog.trace(" validating credentials");
if (info == null || credentials == null)
return false;
String password = info.getPassword();
return getCredentialHandler().matches(credentials, password);
}
/**
* Check credentials by binding to the directory as the user
*
* @param context The directory context
* @param user The User to be authenticated
* @param credentials Authentication credentials
* @return true if the credentials are validated
* @exception NamingException if a directory server error occurs
*/
protected boolean bindAsUser(DirContext context,
User user,
String credentials)
throws NamingException {
if (credentials == null || user == null)
return false;
String dn = user.getDN();
if (dn == null)
return false;
// Validate the credentials specified by the user
if (containerLog.isTraceEnabled()) {
containerLog.trace(" validating credentials by binding as the user");
}
userCredentialsAdd(context, dn, credentials);
// Elicit an LDAP bind operation
boolean validated = false;
try {
if (containerLog.isTraceEnabled()) {
containerLog.trace(" binding as " + dn);
}
context.getAttributes("", null);
validated = true;
}
catch (AuthenticationException e) {
if (containerLog.isTraceEnabled()) {
containerLog.trace(" bind attempt failed");
}
}
userCredentialsRemove(context);
return validated;
}
/**
* Configure the context to use the provided credentials for
* authentication.
*
* @param context DirContext to configure
* @param dn Distinguished name of user
* @param credentials Credentials of user
* @exception NamingException if a directory server error occurs
*/
private void userCredentialsAdd(DirContext context, String dn,
String credentials) throws NamingException {
// Set up security environment to bind as the user
context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
}
/**
* Configure the context to use {@link #connectionName} and
* {@link #connectionPassword} if specified or an anonymous connection if
* those attributes are not specified.
*
* @param context DirContext to configure
* @exception NamingException if a directory server error occurs
*/
private void userCredentialsRemove(DirContext context)
throws NamingException {
// Restore the original security environment
if (connectionName != null) {
context.addToEnvironment(Context.SECURITY_PRINCIPAL,
connectionName);
} else {
context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
}
if (connectionPassword != null) {
context.addToEnvironment(Context.SECURITY_CREDENTIALS,
connectionPassword);
}
else {
context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
}
}
/**
* Return a List of roles associated with the given User. Any
* roles present in the user's directory entry are supplemented by
* a directory search. If no roles are associated with this user,
* a zero-length List is returned.
*
* @param context The directory context we are searching
* @param user The User to be checked
* @return the list of role names
* @exception NamingException if a directory server error occurs
*/
protected List