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     */
019    package org.apache.shiro.realm.ldap;
020    
021    import org.apache.shiro.util.StringUtils;
022    import org.slf4j.Logger;
023    import org.slf4j.LoggerFactory;
024    
025    import javax.naming.AuthenticationException;
026    import javax.naming.Context;
027    import javax.naming.NamingException;
028    import javax.naming.ldap.InitialLdapContext;
029    import javax.naming.ldap.LdapContext;
030    import java.util.HashMap;
031    import java.util.Hashtable;
032    import 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     */
079    public 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    }