View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.realm.ldap;
20  
21  import org.apache.shiro.util.StringUtils;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import javax.naming.AuthenticationException;
26  import javax.naming.Context;
27  import javax.naming.NamingException;
28  import javax.naming.ldap.InitialLdapContext;
29  import javax.naming.ldap.LdapContext;
30  import java.util.HashMap;
31  import java.util.Hashtable;
32  import java.util.Map;
33  
34  /**
35   * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI
36   * environment properties and an {@link javax.naming.InitialContext}.
37   * <h2>Configuration</h2>
38   * This class basically wraps a default template JNDI environment properties Map.  This properties map is the base
39   * configuration template used to acquire JNDI {@link LdapContext} connections at runtime.  The
40   * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties
41   * accessible at runtime only (for example per-method principals and credentials).  The constructed runtime map is the
42   * one used to acquire the {@link LdapContext}.
43   * <p/>
44   * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)}
45   * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods
46   * for various environment properties.  These wrapper methods interact with the environment
47   * template on your behalf, leaving your configuration cleaner and easier to understand.
48   * <p/>
49   * For example, consider the following two identical configurations:
50   * <pre>
51   * [main]
52   * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm
53   * ldapRealm.contextFactory.url = ldap://localhost:389
54   * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
55   * </pre>
56   * and
57   * <pre>
58   * [main]
59   * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm
60   * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389
61   * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5
62   * </pre>
63   * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge
64   * of the underlying JNDI Context property keys.  The first is easier to read and understand.
65   * <p/>
66   * Note that occasionally it will be necessary to use the latter configuration style to set environment properties
67   * where no corresponding wrapper method exists.  In this case, the hybrid approach is still a little easier to read.
68   * For example:
69   * <pre>
70   * [main]
71   * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm
72   * ldapRealm.contextFactory.url = ldap://localhost:389
73   * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
74   * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value
75   * </pre>
76   *
77   * @since 1.1
78   */
79  public class JndiLdapContextFactory implements LdapContextFactory {
80  
81      /*-------------------------------------------
82       |             C O N S T A N T S            |
83       ===========================================*/
84      /**
85       * The Sun LDAP property used to enable connection pooling.  This is used in the default implementation
86       * to enable LDAP connection pooling.
87       */
88      protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
89      protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory";
90      protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple";
91      protected static final String DEFAULT_REFERRAL = "follow";
92  
93      private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class);
94  
95      /*-------------------------------------------
96       |    I N S T A N C E   V A R I A B L E S   |
97       ============================================*/
98      private Map<String, Object> environment;
99      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 }