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.authc.AuthenticationException;
022import org.apache.shiro.authc.AuthenticationInfo;
023import org.apache.shiro.authc.AuthenticationToken;
024import org.apache.shiro.authc.SimpleAuthenticationInfo;
025import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
026import org.apache.shiro.authz.AuthorizationException;
027import org.apache.shiro.authz.AuthorizationInfo;
028import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException;
029import org.apache.shiro.realm.AuthorizingRealm;
030import org.apache.shiro.subject.PrincipalCollection;
031import org.apache.shiro.util.StringUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import javax.naming.AuthenticationNotSupportedException;
036import javax.naming.NamingException;
037import javax.naming.ldap.LdapContext;
038
039/**
040 * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
041 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>.  This is
042 * Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP
043 * support for many years.
044 * <p/>
045 * This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP
046 * authentication and authorization needs.  However, if it does not suit your needs, you might want to look into
047 * creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the
048 * <a href="http://directory.apache.org/api/">Apache LDAP API</a>.
049 * <h2>Authentication</h2>
050 * During an authentication attempt, if the submitted {@code AuthenticationToken}'s
051 * {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the
052 * LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the
053 * {@link #setUserDnTemplate(String) userDnTemplate} property must be configured.  If not configured,
054 * the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP
055 * environments (maybe Microsoft ActiveDirectory being the exception).
056 * <h2>Authorization</h2>
057 * By default, authorization is effectively disabled due to the default
058 * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}.
059 * If you wish to perform authorization based on an LDAP schema, you must subclass this one
060 * and override that method to reflect your organization's data model.
061 * <h2>Configuration</h2>
062 * This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify
063 * the your LDAP server's User DN format.  Most other configuration is performed via the nested
064 * {@link LdapContextFactory contextFactory} property.
065 * <p/>
066 * For example, defining this realm in Shiro .ini:
067 * <pre>
068 * [main]
069 * ldapRealm = org.apache.shiro.realm.ldap.JndiLdapRealm
070 * ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
071 * ldapRealm.contextFactory.url = ldap://ldapHost:389
072 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5
073 * ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value
074 * ...
075 * </pre>
076 * The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}.  See that
077 * class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment
078 * properties as necessary.
079 *
080 * @see JndiLdapContextFactory
081 *
082 * @since 1.1
083 */
084public class JndiLdapRealm extends AuthorizingRealm {
085
086    private static final Logger log = LoggerFactory.getLogger(JndiLdapRealm.class);
087
088    //The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
089    private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";
090
091    private String userDnPrefix;
092    private String userDnSuffix;
093
094    /*--------------------------------------------
095    |    I N S T A N C E   V A R I A B L E S    |
096    ============================================*/
097    /**
098     * The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime
099     * 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}