001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.realm.ldap;
020
021import org.apache.shiro.util.StringUtils;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025import javax.naming.AuthenticationException;
026import javax.naming.Context;
027import javax.naming.NamingException;
028import javax.naming.ldap.InitialLdapContext;
029import javax.naming.ldap.LdapContext;
030import java.util.HashMap;
031import java.util.Hashtable;
032import java.util.Map;
033
034/**
035 * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI
036 * environment properties and an {@link javax.naming.InitialContext}.
037 * <h2>Configuration</h2>
038 * This class basically wraps a default template JNDI environment properties Map.  This properties map is the base
039 * configuration template used to acquire JNDI {@link LdapContext} connections at runtime.  The
040 * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties
041 * accessible at runtime only (for example per-method principals and credentials).  The constructed runtime map is the
042 * one used to acquire the {@link LdapContext}.
043 * <p/>
044 * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)}
045 * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods
046 * for various environment properties.  These wrapper methods interact with the environment
047 * template on your behalf, leaving your configuration cleaner and easier to understand.
048 * <p/>
049 * For example, consider the following two identical configurations:
050 * <pre>
051 * [main]
052 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
053 * ldapRealm.contextFactory.url = ldap://localhost:389
054 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
055 * </pre>
056 * and
057 * <pre>
058 * [main]
059 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
060 * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389
061 * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5
062 * </pre>
063 * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge
064 * of the underlying JNDI Context property keys.  The first is easier to read and understand.
065 * <p/>
066 * Note that occasionally it will be necessary to use the latter configuration style to set environment properties
067 * where no corresponding wrapper method exists.  In this case, the hybrid approach is still a little easier to read.
068 * For example:
069 * <pre>
070 * [main]
071 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
072 * ldapRealm.contextFactory.url = ldap://localhost:389
073 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
074 * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value
075 * </pre>
076 *
077 * @since 1.1
078 */
079public class JndiLdapContextFactory implements LdapContextFactory {
080
081    /*-------------------------------------------
082     |             C O N S T A N T S            |
083     ===========================================*/
084    /**
085     * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
086     * to enable LDAP connection pooling.
087     */
088    protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
089    protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory";
090    protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
091    protected static final String DEFAULT_REFERRAL = "follow";
092
093    private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class);
094
095    /*-------------------------------------------
096     |    I N S T A N C E   V A R I A B L E S   |
097     ============================================*/
098    private Map<String, Object> environment;
099    private boolean poolingEnabled;
100    private String systemPassword;
101    private String systemUsername;
102
103    /*-------------------------------------------
104     |         C O N S T R U C T O R S          |
105     ===========================================*/
106
107    /**
108     * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with
109     * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to
110     * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default
111     * {@link #setReferral(String) referral} behavior to {@code follow}.
112     */
113    public JndiLdapContextFactory() {
114        this.environment = new HashMap<String, Object>();
115        setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME);
116        setReferral(DEFAULT_REFERRAL);
117        poolingEnabled = true;
118    }
119
120    /*-------------------------------------------
121     |  A C C E S S O R S / M O D I F I E R S   |
122     ===========================================*/
123
124    /**
125     * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server.
126     * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s
127     * {@link Context#SECURITY_AUTHENTICATION} property.
128     * <p/>
129     * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured
130     * via this property.  However, if you require a different mechanism, such as a SASL or External mechanism, you
131     * must configure that explicitly via this property.  See the
132     * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
133     * Authentication Mechanisms</a> for more information.
134     *
135     * @param authenticationMechanism the type of LDAP authentication to perform.
136     * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
137     *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
138     */
139    public void setAuthenticationMechanism(String authenticationMechanism) {
140        setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism);
141    }
142
143    /**
144     * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server.
145     * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s
146     * {@link Context#SECURITY_AUTHENTICATION} property.
147     * <p/>
148     * If this property remains un-configured (i.e. {@code null} indicating the
149     * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI
150     * "none" (anonymous) and "simple" authentications are supported automatically.  Any non-null value returned
151     * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the
152     * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP
153     * Authentication Mechanisms</a> for more information.
154     *
155     * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server.
156     * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
157     *      http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a>
158     */
159    public String getAuthenticationMechanism() {
160        return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION);
161    }
162
163    /**
164     * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
165     * but can be overridden to use custom LDAP factories.
166     * <p/>
167     * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
168     *
169     * @param contextFactoryClassName the context factory that should be used.
170     */
171    public void setContextFactoryClassName(String contextFactoryClassName) {
172        setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName);
173    }
174
175    /**
176     * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation
177     * but can be overridden to use custom LDAP factories.
178     * <p/>
179     * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property.
180     *
181     * @return the name of the ContextFactory class to use.
182     */
183    public String getContextFactoryClassName() {
184        return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY);
185    }
186
187    /**
188     * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}).
189     * This property is the base configuration template to use for all connections.  This template is then
190     * merged with appropriate runtime values as necessary in the
191     * {@link #getLdapContext(Object, Object)} implementation.  The merged environment instance is what is used to
192     * acquire the {@link LdapContext} at runtime.
193     * <p/>
194     * Most other get/set methods in this class act as thin proxy wrappers that interact with this property.  The
195     * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties
196     * based on JNDI context keys.
197     *
198     * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext})
199     */
200    public Map getEnvironment() {
201        return this.environment;
202    }
203
204    /**
205     * Sets the base JNDI environment template to use when acquiring LDAP connections.  It is typically more common
206     * to use the other get/set methods in this class to set individual environment settings rather than use
207     * this method, but it is available for advanced users that want full control over the base JNDI environment
208     * settings.
209     * <p/>
210     * Note that this template only represents the base/default environment settings.  It is then merged with
211     * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation.
212     * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime.
213     *
214     * @param env the base JNDI environment template to use when acquiring LDAP connections.
215     */
216    @SuppressWarnings({"unchecked"})
217    public void setEnvironment(Map env) {
218        this.environment = env;
219    }
220
221    /**
222     * Returns the environment property value bound under the specified key.
223     *
224     * @param name the name of the environment property
225     * @return the property value or {@code null} if the value has not been set.
226     */
227    private Object getEnvironmentProperty(String name) {
228        return this.environment.get(name);
229    }
230
231    /**
232     * Will apply the value to the environment attribute if and only if the value is not null or empty.  If it is
233     * null or empty, the corresponding environment attribute will be removed.
234     *
235     * @param name  the environment property key
236     * @param value the environment property value.  A null/empty value will trigger removal.
237     */
238    private void setEnvironmentProperty(String name, String value) {
239        if (StringUtils.hasText(value)) {
240            this.environment.put(name, value);
241        } else {
242            this.environment.remove(name);
243        }
244    }
245
246    /**
247     * Returns whether or not connection pooling should be used when possible and appropriate.  This property is NOT
248     * backed by the {@link #getEnvironment() environment template} like most other properties in this class.  It
249     * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
250     * <p/>
251     * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
252     * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
253     * general authentication attempts by application end-users because the probability of re-use for that same
254     * user-specific connection after an authentication attempt is extremely low.
255     * <p/>
256     * If this attribute is {@code true} and it has been determined that the connection is being made with the
257     * {@link #getSystemUsername() systemUsername}, the
258     * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
259     * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
260     * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
261     * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
262     *
263     * @return whether or not connection pooling should be used when possible and appropriate
264     */
265    public boolean isPoolingEnabled() {
266        return poolingEnabled;
267    }
268
269    /**
270     * Sets whether or not connection pooling should be used when possible and appropriate.  This property is NOT
271     * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class.  It
272     * is a flag to indicate that pooling is preferred.  The default value is {@code true}.
273     * <p/>
274     * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection
275     * being created is for the {@link #getSystemUsername() systemUsername} user.  Connection pooling is not used for
276     * general authentication attempts by application end-users because the probability of re-use for that same
277     * user-specific connection after an authentication attempt is extremely low.
278     * <p/>
279     * If this attribute is {@code true} and it has been determined that the connection is being made with the
280     * {@link #getSystemUsername() systemUsername}, the
281     * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific
282     * {@code com.sun.jndi.ldap.connect.pool} environment property to &quot;{@code true}&quot;.  This means setting
283     * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using
284     * a custom {@link #getContextFactoryClassName() contextFactoryClassName}).
285     *
286     * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate
287     */
288    public void setPoolingEnabled(boolean poolingEnabled) {
289        this.poolingEnabled = poolingEnabled;
290    }
291
292    /**
293     * Sets the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.  See the Sun/Oracle LDAP
294     * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
295     *
296     * @param referral the referral property.
297     * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
298     */
299    public void setReferral(String referral) {
300        setEnvironmentProperty(Context.REFERRAL, referral);
301    }
302
303    /**
304     * Returns the LDAP referral behavior when creating a connection.  Defaults to {@code follow}.
305     * See the Sun/Oracle LDAP
306     * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more.
307     *
308     * @return the LDAP referral behavior when creating a connection.
309     * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a>
310     */
311    public String getReferral() {
312        return (String) getEnvironmentProperty(Context.REFERRAL);
313    }
314
315    /**
316     * The LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).  This must be configured.
317     *
318     * @param url the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
319     */
320    public void setUrl(String url) {
321        setEnvironmentProperty(Context.PROVIDER_URL, url);
322    }
323
324    /**
325     * Returns the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;).
326     * This must be configured.
327     *
328     * @return the LDAP url to connect to. (e.g. ldap://&lt;ldapDirectoryHostname&gt;:&lt;port&gt;)
329     */
330    public String getUrl() {
331        return (String) getEnvironmentProperty(Context.PROVIDER_URL);
332    }
333
334    /**
335     * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
336     * LDAP connection used for authorization queries.
337     * <p/>
338     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
339     * checks.
340     *
341     * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used
342     *                       when creating an LDAP connection used for authorization queries.
343     */
344    public void setSystemPassword(String systemPassword) {
345        this.systemPassword = systemPassword;
346    }
347
348    /**
349     * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
350     * LDAP connection used for authorization queries.
351     * <p/>
352     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
353     * checks.
354     *
355     * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an
356     *         LDAP connection used for authorization queries.
357     */
358    public String getSystemPassword() {
359        return this.systemPassword;
360    }
361
362    /**
363     * Sets the system username that will be used when creating an LDAP connection used for authorization queries.
364     * The user must have the ability to query for authorization data for any application user.
365     * <p/>
366     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
367     * checks.
368     *
369     * @param systemUsername the system username that will be used when creating an LDAP connection used for
370     *                       authorization queries.
371     */
372    public void setSystemUsername(String systemUsername) {
373        this.systemUsername = systemUsername;
374    }
375
376    /**
377     * Returns the system username that will be used when creating an LDAP connection used for authorization queries.
378     * The user must have the ability to query for authorization data for any application user.
379     * <p/>
380     * Note that setting this property is not required if the calling LDAP Realm does not perform authorization
381     * checks.
382     *
383     * @return the system username that will be used when creating an LDAP connection used for authorization queries.
384     */
385    public String getSystemUsername() {
386        return systemUsername;
387    }
388
389    /*--------------------------------------------
390    |               M E T H O D S               |
391    ============================================*/
392
393    /**
394     * This implementation delegates to {@link #getLdapContext(Object, Object)} using the
395     * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as
396     * arguments.
397     *
398     * @return the system LdapContext
399     * @throws NamingException if there is a problem connecting to the LDAP directory
400     */
401    public LdapContext getSystemLdapContext() throws NamingException {
402        return getLdapContext((Object)getSystemUsername(), getSystemPassword());
403    }
404
405    /**
406     * Deprecated - use {@link #getLdapContext(Object, Object)} instead.  This will be removed before Apache Shiro 2.0.
407     *
408     * @param username the username to use when creating the connection.
409     * @param password the password to use when creating the connection.
410     * @return a {@code LdapContext} bound using the given username and password.
411     * @throws javax.naming.NamingException if there is an error creating the context.
412     * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than
413     *             String principals and credentials can be used.  Shiro no longer calls this method - it will be
414     *             removed before the 2.0 release.
415     */
416    @Deprecated
417    public LdapContext getLdapContext(String username, String password) throws NamingException {
418        return getLdapContext((Object) username, password);
419    }
420
421    /**
422     * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
423     * account principal, {@code false} otherwise.
424     * <p/>
425     * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the
426     * {@link #getSystemUsername()}.  The reasoning behind this is that connection pooling is not desirable for
427     * general authentication attempts by application end-users because the probability of re-use for that same
428     * user-specific connection after an authentication attempt is extremely low.
429     *
430     * @param principal the principal under which the connection will be made
431     * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified
432     *         account principal, {@code false} otherwise.
433     */
434    protected boolean isPoolingConnections(Object principal) {
435        return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername());
436    }
437
438    /**
439     * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration.
440     * The environnmet (Map) used at runtime is created by merging the default/configured
441     * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and
442     * credential available at runtime only).
443     * <p/>
444     * After the merged Map instance is created, the LdapContext connection is
445     * {@link #createLdapContext(java.util.Hashtable) created} and returned.
446     *
447     * @param principal   the principal to use when acquiring a connection to the LDAP directory
448     * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the
449     *                    LDAP directory
450     * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials.
451     * @throws NamingException
452     * @throws IllegalStateException
453     */
454    public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException,
455            IllegalStateException {
456
457        String url = getUrl();
458        if (url == null) {
459            throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>");
460        }
461
462        //copy the environment template into the runtime instance that will be further edited based on
463        //the method arguments and other class attributes.
464        Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment);
465
466        Object authcMech = getAuthenticationMechanism();
467        if (authcMech == null && (principal != null || credentials != null)) {
468            //authenticationMechanism has not been set, but either a principal and/or credentials were
469            //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro
470            //end-user just didn't configure it explicitly.  So we set it to be 'simple' here as a convenience;
471            //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure
472            //this convenience exists regardless of provider implementation):
473            env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME);
474        }
475        if (principal != null) {
476            env.put(Context.SECURITY_PRINCIPAL, principal);
477        }
478        if (credentials != null) {
479            env.put(Context.SECURITY_CREDENTIALS, credentials);
480        }
481
482        boolean pooling = isPoolingConnections(principal);
483        if (pooling) {
484            env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
485        }
486
487        if (log.isDebugEnabled()) {
488            log.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}",
489                    new Object[]{url, principal, (pooling ? "enabled" : "disabled")});
490        }
491
492        // validate the config before creating the context
493        validateAuthenticationInfo(env);
494
495        return createLdapContext(env);
496    }
497
498    /**
499     * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance.  This method exists primarily
500     * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but
501     * subclasses are free to provide a different implementation if necessary.
502     *
503     * @param env the JNDI environment settings used to create the LDAP connection
504     * @return an LdapConnection
505     * @throws NamingException if a problem occurs creating the connection
506     */
507    protected LdapContext createLdapContext(Hashtable env) throws NamingException {
508        return new InitialLdapContext(env, null);
509    }
510
511
512    /**
513     * Validates the configuration in the JNDI <code>environment</code> settings and throws an exception if a problem
514     * exists.
515     * <p/>
516     * This implementation will throw a {@link AuthenticationException} if the authentication mechanism is set to
517     * 'simple', the principal is non-empty, and the credentials are empty (as per
518     * <a href="http://tools.ietf.org/html/rfc4513#section-5.1.2">rfc4513 section-5.1.2</a>).
519     *
520     * @param environment the JNDI environment settings to be validated
521     * @throws AuthenticationException if a configuration problem is detected
522     */
523    protected void validateAuthenticationInfo(Hashtable<String, Object> environment)
524        throws AuthenticationException
525    {
526        // validate when using Simple auth both principal and credentials are set
527        if(SIMPLE_AUTHENTICATION_MECHANISM_NAME.equals(environment.get(Context.SECURITY_AUTHENTICATION))) {
528
529            // only validate credentials if we have a non-empty principal
530            if( environment.get(Context.SECURITY_PRINCIPAL) != null &&
531                StringUtils.hasText( String.valueOf( environment.get(Context.SECURITY_PRINCIPAL) ))) {
532
533                Object credentials = environment.get(Context.SECURITY_CREDENTIALS);
534
535                // from the FAQ, we need to check for empty credentials:
536                // http://docs.oracle.com/javase/tutorial/jndi/ldap/faq.html
537                if( credentials == null ||
538                    (credentials instanceof byte[] && ((byte[])credentials).length <= 0) || // empty byte[]
539                    (credentials instanceof char[] && ((char[])credentials).length <= 0) || // empty char[]
540                    (String.class.isInstance(credentials) && !StringUtils.hasText(String.valueOf(credentials)))) {
541
542                    throw new javax.naming.AuthenticationException("LDAP Simple authentication requires both a "
543                                                                       + "principal and credentials.");
544                }
545            }
546        }
547    }
548
549}