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.authc.AuthenticationException;
22  import org.apache.shiro.authc.AuthenticationInfo;
23  import org.apache.shiro.authc.AuthenticationToken;
24  import org.apache.shiro.authc.SimpleAuthenticationInfo;
25  import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
26  import org.apache.shiro.authz.AuthorizationException;
27  import org.apache.shiro.authz.AuthorizationInfo;
28  import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException;
29  import org.apache.shiro.realm.AuthorizingRealm;
30  import org.apache.shiro.subject.PrincipalCollection;
31  import org.apache.shiro.util.StringUtils;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  import javax.naming.AuthenticationNotSupportedException;
36  import javax.naming.NamingException;
37  import javax.naming.ldap.LdapContext;
38  
39  /**
40   * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
41   * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>.  This is
42   * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP
43   * support for many years.
44   * <p/>
45   * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP
46   * authentication and authorization needs.  However, if it does not suit your needs, you might want to look into
47   * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the
48   * <a href="http://directory.apache.org/api/">Apache LDAP API</a>.
49   * <h2>Authentication</h2>
50   * During an authentication attempt, if the submitted {@code AuthenticationToken}'s
51   * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the
52   * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the
53   * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured.  If not configured,
54   * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP
55   * environments (maybe Microsoft ActiveDirectory being the exception).
56   * <h2>Authorization</h2>
57   * By default, authorization is effectively disabled due to the default
58   * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}.
59   * If you wish to perform authorization based on an LDAP schema, you must subclass this one
60   * and override that method to reflect your organization's data model.
61   * <h2>Configuration</h2>
62   * This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify
63   * the your LDAP server's User DN format.  Most other configuration is performed via the nested
64   * {@link LdapContextFactory contextFactory} property.
65   * <p/>
66   * For example, defining this realm in Shiro .ini:
67   * <pre>
68   * [main]
69   * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
70   * ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
71   * ldapRealm.contextFactory.url = ldap://ldapHost:389
72   * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
73   * ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value
74   * ...
75   * </pre>
76   * The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}.  See that
77   * class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment
78   * properties as necessary.
79   *
80   * @see JndiLdapContextFactory
81   *
82   * @since 1.1
83   */
84  public class JndiLdapRealm extends AuthorizingRealm {
85  
86      private static final Logger log = LoggerFactory.getLogger(JndiLdapRealm.class);
87  
88      //The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
89      private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";
90  
91      private String userDnPrefix;
92      private String userDnSuffix;
93  
94      /*--------------------------------------------
95      |    I N S T A N C E   V A R I A B L E S    |
96      ============================================*/
97      /**
98       * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime
99       * to acquire connections to the LDAP directory to perform authentication attempts and authorizatino queries.
100      */
101     private LdapContextFactory contextFactory;
102 
103     /*--------------------------------------------
104     |         C O N S T R U C T O R S           |
105     ============================================*/
106 
107     /**
108      * Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a
109      * {@link JndiLdapContextFactory}.
110      */
111     public JndiLdapRealm() {
112         //Credentials Matching is not necessary - the LDAP directory will do it automatically:
113         setCredentialsMatcher(new AllowAllCredentialsMatcher());
114         //Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token:
115         setAuthenticationTokenClass(AuthenticationToken.class);
116         this.contextFactory = new JndiLdapContextFactory();
117     }
118 
119     /*--------------------------------------------
120     |  A C C E S S O R S / M O D I F I E R S    |
121     ============================================*/
122 
123     /**
124      * Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no
125      * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
126      * occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
127      *
128      * @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no
129      *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
130      */
131     protected String getUserDnPrefix() {
132         return userDnPrefix;
133     }
134 
135     /**
136      * Returns the User DN suffix to use when building a runtime User DN value.  or {@code null} if no
137      * {@link #getUserDnTemplate() userDnTemplate} has been configured.  If configured, this value is the text that
138      * occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
139      *
140      * @return the User DN suffix to use when building a runtime User DN value or {@code null} if no
141      *         {@link #getUserDnTemplate() userDnTemplate} has been configured.
142      */
143     protected String getUserDnSuffix() {
144         return userDnSuffix;
145     }
146 
147     /*--------------------------------------------
148     |               M E T H O D S               |
149     ============================================*/
150 
151     /**
152      * Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime.  A User DN is an LDAP
153      * fully-qualified unique user identifier which is required to establish a connection with the LDAP
154      * directory to authenticate users and query for authorization information.
155      * <h2>Usage</h2>
156      * User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to
157      * specify the format corresponding to your directory.  You do this by specifying the full User DN as normal, but
158      * but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the
159      * user's submitted principal (usually a username or uid) will be substituted at runtime.
160      * <p/>
161      * For example,  if your directory
162      * uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like
163      * this:
164      * <p/>
165      * <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre>
166      * <p/>
167      * in which case you would set this property with the following template value:
168      * <p/>
169      * <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre>
170      * <p/>
171      * If no template is configured, the raw {@code AuthenticationToken}
172      * {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal.  This is likely
173      * incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username.  So,
174      * ensure you set this property to match your environment!
175      *
176      * @param template the User Distinguished Name template to use for runtime substitution
177      * @throws IllegalArgumentException if the template is null, empty, or does not contain the
178      *                                  {@code {0}} substitution token.
179      * @see LdapContextFactory#getLdapContext(Object,Object)
180      */
181     public void setUserDnTemplate(String template) throws IllegalArgumentException {
182         if (!StringUtils.hasText(template)) {
183             String msg = "User DN template cannot be null or empty.";
184             throw new IllegalArgumentException(msg);
185         }
186         int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN);
187         if (index < 0) {
188             String msg = "User DN template must contain the '" +
189                     USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " +
190                     "insert the runtime authentication principal.";
191             throw new IllegalArgumentException(msg);
192         }
193         String prefix = template.substring(0, index);
194         String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length());
195         if (log.isDebugEnabled()) {
196             log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix);
197         }
198         this.userDnPrefix = prefix;
199         this.userDnSuffix = suffix;
200     }
201 
202     /**
203      * Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the
204      * {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation.
205      *
206      * @return the User Distinguished Name (DN) template to use when creating User DNs at runtime.
207      */
208     public String getUserDnTemplate() {
209         return getUserDn(USERDN_SUBSTITUTION_TOKEN);
210     }
211 
212     /**
213      * Returns the LDAP User Distinguished Name (DN) to use when acquiring an
214      * {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}.
215      * <p/>
216      * If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct
217      * the User DN by substituting the specified {@code principal} into the configured template.  If the
218      * {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly
219      * (indicating that the submitted authentication token principal <em>is</em> the User DN).
220      *
221      * @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}.
222      * @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}.
223      * @throws IllegalArgumentException if the method argument is null or empty
224      * @throws IllegalStateException    if the {@link #getUserDnTemplate userDnTemplate} has not been set.
225      * @see LdapContextFactory#getLdapContext(Object, Object)
226      */
227     protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException {
228         if (!StringUtils.hasText(principal)) {
229             throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
230         }
231         String prefix = getUserDnPrefix();
232         String suffix = getUserDnSuffix();
233         if (prefix == null && suffix == null) {
234             log.debug("userDnTemplate property has not been configured, indicating the submitted " +
235                     "AuthenticationToken's principal is the same as the User DN.  Returning the method argument " +
236                     "as is.");
237             return principal;
238         }
239 
240         int prefixLength = prefix != null ? prefix.length() : 0;
241         int suffixLength = suffix != null ? suffix.length() : 0;
242         StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
243         if (prefixLength > 0) {
244             sb.append(prefix);
245         }
246         sb.append(principal);
247         if (suffixLength > 0) {
248             sb.append(suffix);
249         }
250         return sb.toString();
251     }
252 
253     /**
254      * Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
255      * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
256      * instance.
257      *
258      * @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during
259      *                       authentication attempts and authorization queries
260      */
261     @SuppressWarnings({"UnusedDeclaration"})
262     public void setContextFactory(LdapContextFactory contextFactory) {
263         this.contextFactory = contextFactory;
264     }
265 
266     /**
267      * Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication
268      * attempts and authorization queries.  Unless specified otherwise, the default is a {@link JndiLdapContextFactory}
269      * instance.
270      *
271      * @return the LdapContextFactory instance used to acquire connections to the LDAP directory during
272      *         authentication attempts and authorization queries
273      */
274     public LdapContextFactory getContextFactory() {
275         return this.contextFactory;
276     }
277 
278     /*--------------------------------------------
279     |               M E T H O D S                |
280     ============================================*/
281 
282     /**
283      * Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)},
284      * wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method
285      * signature.
286      *
287      * @param token the authentication token containing the user's principal and credentials.
288      * @return the {@link AuthenticationInfo} acquired after a successful authentication attempt
289      * @throws AuthenticationException if the authentication attempt fails or if a
290      *                                 {@link NamingException} occurs.
291      */
292     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
293         AuthenticationInfo info;
294         try {
295             info = queryForAuthenticationInfo(token, getContextFactory());
296         } catch (AuthenticationNotSupportedException e) {
297             String msg = "Unsupported configured authentication mechanism";
298             throw new UnsupportedAuthenticationMechanismException(msg, e);
299         } catch (javax.naming.AuthenticationException e) {
300             throw new AuthenticationException("LDAP authentication failed.", e);
301         } catch (NamingException e) {
302             String msg = "LDAP naming error while attempting to authenticate user.";
303             throw new AuthenticationException(msg, e);
304         }
305 
306         return info;
307     }
308 
309 
310     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
311         AuthorizationInfo info;
312         try {
313             info = queryForAuthorizationInfo(principals, getContextFactory());
314         } catch (NamingException e) {
315             String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "].";
316             throw new AuthorizationException(msg, e);
317         }
318 
319         return info;
320     }
321 
322     /**
323      * Returns the principal to use when creating the LDAP connection for an authentication attempt.
324      * <p/>
325      * This implementation uses a heuristic: it checks to see if the specified token's
326      * {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so,
327      * {@link #getUserDn(String) converts it} from what is
328      * assumed to be a raw uid or username {@code String} into a User DN {@code String}.  Almost all LDAP directories
329      * expect the authentication connection to present a User DN and not an unqualified username or uid.
330      * <p/>
331      * If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the
332      * underlying {@link LdapContextFactory} implementation and the raw principal is returned directly.
333      *
334      * @param token the {@link AuthenticationToken} submitted during the authentication process
335      * @return the User DN or raw principal to use to acquire the LdapContext.
336      * @see LdapContextFactory#getLdapContext(Object, Object)
337      */
338     protected Object getLdapPrincipal(AuthenticationToken token) {
339         Object principal = token.getPrincipal();
340         if (principal instanceof String) {
341             String sPrincipal = (String) principal;
342             return getUserDn(sPrincipal);
343         }
344         return principal;
345     }
346 
347     /**
348      * This implementation opens an LDAP connection using the token's
349      * {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided
350      * {@link AuthenticationToken#getCredentials() credentials}.  If the connection opens successfully, the
351      * authentication attempt is immediately considered successful and a new
352      * {@link AuthenticationInfo} instance is
353      * {@link #createAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, Object, Object, javax.naming.ldap.LdapContext) created}
354      * and returned.  If the connection cannot be opened, either because LDAP authentication failed or some other
355      * JNDI problem, an {@link NamingException} will be thrown.
356      *
357      * @param token              the submitted authentication token that triggered the authentication attempt.
358      * @param ldapContextFactory factory used to retrieve LDAP connections.
359      * @return an {@link AuthenticationInfo} instance representing the authenticated user's information.
360      * @throws NamingException if any LDAP errors occur.
361      */
362     protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token,
363                                                             LdapContextFactory ldapContextFactory)
364             throws NamingException {
365 
366         Object principal = token.getPrincipal();
367         Object credentials = token.getCredentials();
368 
369         log.debug("Authenticating user '{}' through LDAP", principal);
370 
371         principal = getLdapPrincipal(token);
372 
373         LdapContext ctx = null;
374         try {
375             ctx = ldapContextFactory.getLdapContext(principal, credentials);
376             //context was opened successfully, which means their credentials were valid.  Return the AuthenticationInfo:
377             return createAuthenticationInfo(token, principal, credentials, ctx);
378         } finally {
379             LdapUtils.closeContext(ctx);
380         }
381     }
382 
383     /**
384      * Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
385      * <p/>
386      * This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened
387      * {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the
388      * submitted token's principal and credentials.  This is acceptable because this method is only ever invoked after
389      * a successful authentication attempt, which means the provided principal and credentials were correct, and can
390      * be used directly to populate the (now verified) {@code AuthenticationInfo}.
391      * <p/>
392      * Subclasses however are free to override this method for more advanced construction logic.
393      *
394      * @param token           the submitted {@code AuthenticationToken} that resulted in a successful authentication
395      * @param ldapPrincipal   the LDAP principal used when creating the LDAP connection.  Unlike the token's
396      *                        {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed
397      *                        User DN and not a simple username or uid.  The exact value is depending on the
398      *                        configured
399      *                        <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">
400      *                        LDAP authentication mechanism</a> in use.
401      * @param ldapCredentials the LDAP credentials used when creating the LDAP connection.
402      * @param ldapContext     the LdapContext created that resulted in a successful authentication.  It can be used
403      *                        further by subclasses for more complex operations.  It does not need to be closed -
404      *                        it will be closed automatically after this method returns.
405      * @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt.
406      * @throws NamingException if there was any problem using the {@code LdapContext}
407      */
408     @SuppressWarnings({"UnusedDeclaration"})
409     protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal,
410                                                           Object ldapCredentials, LdapContext ldapContext)
411             throws NamingException {
412         return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
413     }
414 
415 
416     /**
417      * Method that should be implemented by subclasses to build an
418      * {@link AuthorizationInfo} object by querying the LDAP context for the
419      * specified principal.</p>
420      *
421      * @param principals          the principals of the Subject whose AuthenticationInfo should be queried from the LDAP server.
422      * @param ldapContextFactory factory used to retrieve LDAP connections.
423      * @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server.
424      * @throws NamingException if any LDAP errors occur during the search.
425      */
426     protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
427                                                           LdapContextFactory ldapContextFactory) throws NamingException {
428         return null;
429     }
430 }