/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.shiro.realm.ldap; | |
import org.apache.shiro.authc.AuthenticationException; | |
import org.apache.shiro.authc.AuthenticationInfo; | |
import org.apache.shiro.authc.AuthenticationToken; | |
import org.apache.shiro.authc.SimpleAuthenticationInfo; | |
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; | |
import org.apache.shiro.authz.AuthorizationException; | |
import org.apache.shiro.authz.AuthorizationInfo; | |
import org.apache.shiro.ldap.UnsupportedAuthenticationMechanismException; | |
import org.apache.shiro.realm.AuthorizingRealm; | |
import org.apache.shiro.subject.PrincipalCollection; | |
import org.apache.shiro.lang.util.StringUtils; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import javax.naming.AuthenticationNotSupportedException; | |
import javax.naming.NamingException; | |
import javax.naming.ldap.LdapContext; | |
/** | |
* An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's | |
* <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/jndi.html">JNDI API as an LDAP API</a>. This is | |
* Shiro's default implementation for supporting LDAP, as using the JNDI API has been a common approach for Java LDAP | |
* support for many years. | |
* <p/> | |
* This realm implementation and its backing {@link JndiLdapContextFactory} should cover 99% of all Shiro-related LDAP | |
* authentication and authorization needs. However, if it does not suit your needs, you might want to look into | |
* creating your own realm using an alternative, perhaps more robust, LDAP communication API, such as the | |
* <a href="http://directory.apache.org/api/">Apache LDAP API</a>. | |
* <h2>Authentication</h2> | |
* During an authentication attempt, if the submitted {@code AuthenticationToken}'s | |
* {@link org.apache.shiro.authc.AuthenticationToken#getPrincipal() principal} is a simple username, but the | |
* LDAP directory expects a complete User Distinguished Name (User DN) to establish a connection, the | |
* {@link #setUserDnTemplate(String) userDnTemplate} property must be configured. If not configured, | |
* the property will pass the simple username directly as the User DN, which is often incorrect in most LDAP | |
* environments (maybe Microsoft ActiveDirectory being the exception). | |
* <h2>Authorization</h2> | |
* By default, authorization is effectively disabled due to the default | |
* {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} implementation returning {@code null}. | |
* If you wish to perform authorization based on an LDAP schema, you must subclass this one | |
* and override that method to reflect your organization's data model. | |
* <h2>Configuration</h2> | |
* This class primarily provides the {@link #setUserDnTemplate(String) userDnTemplate} property to allow you to specify | |
* the your LDAP server's User DN format. Most other configuration is performed via the nested | |
* {@link LdapContextFactory contextFactory} property. | |
* <p/> | |
* For example, defining this realm in Shiro .ini: | |
* <pre> | |
* [main] | |
* ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm | |
* ldapRealm.userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com | |
* ldapRealm.contextFactory.url = ldap://ldapHost:389 | |
* ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 | |
* ldapRealm.contextFactory.environment[some.obscure.jndi.key] = some value | |
* ... | |
* </pre> | |
* The default {@link #setContextFactory contextFactory} instance is a {@link JndiLdapContextFactory}. See that | |
* class's JavaDoc for more information on configuring the LDAP connection as well as specifying JNDI environment | |
* properties as necessary. | |
* | |
* @see JndiLdapContextFactory | |
* | |
* @since 1.3 | |
*/ | |
public class DefaultLdapRealm extends AuthorizingRealm { | |
private static final Logger log = LoggerFactory.getLogger(DefaultLdapRealm.class); | |
//The zero index currently means nothing, but could be utilized in the future for other substitution techniques. | |
private static final String USERDN_SUBSTITUTION_TOKEN = "{0}"; | |
private String userDnPrefix; | |
private String userDnSuffix; | |
/*-------------------------------------------- | |
| I N S T A N C E V A R I A B L E S | | |
============================================*/ | |
/** | |
* The LdapContextFactory instance used to acquire {@link javax.naming.ldap.LdapContext LdapContext}'s at runtime | |
* to acquire connections to the LDAP directory to perform authentication attempts and authorization queries. | |
*/ | |
private LdapContextFactory contextFactory; | |
/*-------------------------------------------- | |
| C O N S T R U C T O R S | | |
============================================*/ | |
/** | |
* Default no-argument constructor that defaults the internal {@link LdapContextFactory} instance to a | |
* {@link JndiLdapContextFactory}. | |
*/ | |
public DefaultLdapRealm() { | |
//Credentials Matching is not necessary - the LDAP directory will do it automatically: | |
setCredentialsMatcher(new AllowAllCredentialsMatcher()); | |
//Any Object principal and Object credentials may be passed to the LDAP provider, so accept any token: | |
setAuthenticationTokenClass(AuthenticationToken.class); | |
this.contextFactory = new JndiLdapContextFactory(); | |
} | |
/*-------------------------------------------- | |
| A C C E S S O R S / M O D I F I E R S | | |
============================================*/ | |
/** | |
* Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no | |
* {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that | |
* occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. | |
* | |
* @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no | |
* {@link #getUserDnTemplate() userDnTemplate} has been configured. | |
*/ | |
protected String getUserDnPrefix() { | |
return userDnPrefix; | |
} | |
/** | |
* Returns the User DN suffix to use when building a runtime User DN value. or {@code null} if no | |
* {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that | |
* occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value. | |
* | |
* @return the User DN suffix to use when building a runtime User DN value or {@code null} if no | |
* {@link #getUserDnTemplate() userDnTemplate} has been configured. | |
*/ | |
protected String getUserDnSuffix() { | |
return userDnSuffix; | |
} | |
/*-------------------------------------------- | |
| M E T H O D S | | |
============================================*/ | |
/** | |
* Sets the User Distinguished Name (DN) template to use when creating User DNs at runtime. A User DN is an LDAP | |
* fully-qualified unique user identifier which is required to establish a connection with the LDAP | |
* directory to authenticate users and query for authorization information. | |
* <h2>Usage</h2> | |
* User DN formats are unique to the LDAP directory's schema, and each environment differs - you will need to | |
* specify the format corresponding to your directory. You do this by specifying the full User DN as normal, but | |
* but you use a <b>{@code {0}}</b> placeholder token in the string representing the location where the | |
* user's submitted principal (usually a username or uid) will be substituted at runtime. | |
* <p/> | |
* For example, if your directory | |
* uses an LDAP {@code uid} attribute to represent usernames, the User DN for the {@code jsmith} user may look like | |
* this: | |
* <p/> | |
* <pre>uid=jsmith,ou=users,dc=mycompany,dc=com</pre> | |
* <p/> | |
* in which case you would set this property with the following template value: | |
* <p/> | |
* <pre>uid=<b>{0}</b>,ou=users,dc=mycompany,dc=com</pre> | |
* <p/> | |
* If no template is configured, the raw {@code AuthenticationToken} | |
* {@link AuthenticationToken#getPrincipal() principal} will be used as the LDAP principal. This is likely | |
* incorrect as most LDAP directories expect a fully-qualified User DN as opposed to the raw uid or username. So, | |
* ensure you set this property to match your environment! | |
* | |
* @param template the User Distinguished Name template to use for runtime substitution | |
* @throws IllegalArgumentException if the template is null, empty, or does not contain the | |
* {@code {0}} substitution token. | |
* @see LdapContextFactory#getLdapContext(Object,Object) | |
*/ | |
public void setUserDnTemplate(String template) throws IllegalArgumentException { | |
if (!StringUtils.hasText(template)) { | |
String msg = "User DN template cannot be null or empty."; | |
throw new IllegalArgumentException(msg); | |
} | |
int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN); | |
if (index < 0) { | |
String msg = "User DN template must contain the '" + | |
USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " + | |
"insert the runtime authentication principal."; | |
throw new IllegalArgumentException(msg); | |
} | |
String prefix = template.substring(0, index); | |
String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length()); | |
if (log.isDebugEnabled()) { | |
log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix); | |
} | |
this.userDnPrefix = prefix; | |
this.userDnSuffix = suffix; | |
} | |
/** | |
* Returns the User Distinguished Name (DN) template to use when creating User DNs at runtime - see the | |
* {@link #setUserDnTemplate(String) setUserDnTemplate} JavaDoc for a full explanation. | |
* | |
* @return the User Distinguished Name (DN) template to use when creating User DNs at runtime. | |
*/ | |
public String getUserDnTemplate() { | |
return getUserDn(USERDN_SUBSTITUTION_TOKEN); | |
} | |
/** | |
* Returns the LDAP User Distinguished Name (DN) to use when acquiring an | |
* {@link javax.naming.ldap.LdapContext LdapContext} from the {@link LdapContextFactory}. | |
* <p/> | |
* If the the {@link #getUserDnTemplate() userDnTemplate} property has been set, this implementation will construct | |
* the User DN by substituting the specified {@code principal} into the configured template. If the | |
* {@link #getUserDnTemplate() userDnTemplate} has not been set, the method argument will be returned directly | |
* (indicating that the submitted authentication token principal <em>is</em> the User DN). | |
* | |
* @param principal the principal to substitute into the configured {@link #getUserDnTemplate() userDnTemplate}. | |
* @return the constructed User DN to use at runtime when acquiring an {@link javax.naming.ldap.LdapContext}. | |
* @throws IllegalArgumentException if the method argument is null or empty | |
* @throws IllegalStateException if the {@link #getUserDnTemplate userDnTemplate} has not been set. | |
* @see LdapContextFactory#getLdapContext(Object, Object) | |
*/ | |
protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { | |
if (!StringUtils.hasText(principal)) { | |
throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); | |
} | |
String prefix = getUserDnPrefix(); | |
String suffix = getUserDnSuffix(); | |
if (prefix == null && suffix == null) { | |
log.debug("userDnTemplate property has not been configured, indicating the submitted " + | |
"AuthenticationToken's principal is the same as the User DN. Returning the method argument " + | |
"as is."); | |
return principal; | |
} | |
int prefixLength = prefix != null ? prefix.length() : 0; | |
int suffixLength = suffix != null ? suffix.length() : 0; | |
StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength); | |
if (prefixLength > 0) { | |
sb.append(prefix); | |
} | |
sb.append(principal); | |
if (suffixLength > 0) { | |
sb.append(suffix); | |
} | |
return sb.toString(); | |
} | |
/** | |
* Sets the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication | |
* attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} | |
* instance. | |
* | |
* @param contextFactory the LdapContextFactory instance used to acquire connections to the LDAP directory during | |
* authentication attempts and authorization queries | |
*/ | |
@SuppressWarnings({"UnusedDeclaration"}) | |
public void setContextFactory(LdapContextFactory contextFactory) { | |
this.contextFactory = contextFactory; | |
} | |
/** | |
* Returns the LdapContextFactory instance used to acquire connections to the LDAP directory during authentication | |
* attempts and authorization queries. Unless specified otherwise, the default is a {@link JndiLdapContextFactory} | |
* instance. | |
* | |
* @return the LdapContextFactory instance used to acquire connections to the LDAP directory during | |
* authentication attempts and authorization queries | |
*/ | |
public LdapContextFactory getContextFactory() { | |
return this.contextFactory; | |
} | |
/*-------------------------------------------- | |
| M E T H O D S | | |
============================================*/ | |
/** | |
* Delegates to {@link #queryForAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, LdapContextFactory)}, | |
* wrapping any {@link NamingException}s in a Shiro {@link AuthenticationException} to satisfy the parent method | |
* signature. | |
* | |
* @param token the authentication token containing the user's principal and credentials. | |
* @return the {@link AuthenticationInfo} acquired after a successful authentication attempt | |
* @throws AuthenticationException if the authentication attempt fails or if a | |
* {@link NamingException} occurs. | |
*/ | |
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { | |
AuthenticationInfo info; | |
try { | |
info = queryForAuthenticationInfo(token, getContextFactory()); | |
} catch (AuthenticationNotSupportedException e) { | |
String msg = "Unsupported configured authentication mechanism"; | |
throw new UnsupportedAuthenticationMechanismException(msg, e); | |
} catch (javax.naming.AuthenticationException e) { | |
throw new AuthenticationException("LDAP authentication failed.", e); | |
} catch (NamingException e) { | |
String msg = "LDAP naming error while attempting to authenticate user."; | |
throw new AuthenticationException(msg, e); | |
} | |
return info; | |
} | |
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |
AuthorizationInfo info; | |
try { | |
info = queryForAuthorizationInfo(principals, getContextFactory()); | |
} catch (NamingException e) { | |
String msg = "LDAP naming error while attempting to retrieve authorization for user [" + principals + "]."; | |
throw new AuthorizationException(msg, e); | |
} | |
return info; | |
} | |
/** | |
* Returns the principal to use when creating the LDAP connection for an authentication attempt. | |
* <p/> | |
* This implementation uses a heuristic: it checks to see if the specified token's | |
* {@link AuthenticationToken#getPrincipal() principal} is a {@code String}, and if so, | |
* {@link #getUserDn(String) converts it} from what is | |
* assumed to be a raw uid or username {@code String} into a User DN {@code String}. Almost all LDAP directories | |
* expect the authentication connection to present a User DN and not an unqualified username or uid. | |
* <p/> | |
* If the token's {@code principal} is not a String, it is assumed to already be in the format supported by the | |
* underlying {@link LdapContextFactory} implementation and the raw principal is returned directly. | |
* | |
* @param token the {@link AuthenticationToken} submitted during the authentication process | |
* @return the User DN or raw principal to use to acquire the LdapContext. | |
* @see LdapContextFactory#getLdapContext(Object, Object) | |
*/ | |
protected Object getLdapPrincipal(AuthenticationToken token) { | |
Object principal = token.getPrincipal(); | |
if (principal instanceof String) { | |
String sPrincipal = (String) principal; | |
return getUserDn(sPrincipal); | |
} | |
return principal; | |
} | |
/** | |
* This implementation opens an LDAP connection using the token's | |
* {@link #getLdapPrincipal(org.apache.shiro.authc.AuthenticationToken) discovered principal} and provided | |
* {@link AuthenticationToken#getCredentials() credentials}. If the connection opens successfully, the | |
* authentication attempt is immediately considered successful and a new | |
* {@link AuthenticationInfo} instance is | |
* {@link #createAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken, Object, Object, javax.naming.ldap.LdapContext) created} | |
* and returned. If the connection cannot be opened, either because LDAP authentication failed or some other | |
* JNDI problem, an {@link NamingException} will be thrown. | |
* | |
* @param token the submitted authentication token that triggered the authentication attempt. | |
* @param ldapContextFactory factory used to retrieve LDAP connections. | |
* @return an {@link AuthenticationInfo} instance representing the authenticated user's information. | |
* @throws NamingException if any LDAP errors occur. | |
*/ | |
protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, | |
LdapContextFactory ldapContextFactory) | |
throws NamingException { | |
Object principal = token.getPrincipal(); | |
Object credentials = token.getCredentials(); | |
log.debug("Authenticating user '{}' through LDAP", principal); | |
principal = getLdapPrincipal(token); | |
LdapContext ctx = null; | |
try { | |
ctx = ldapContextFactory.getLdapContext(principal, credentials); | |
//context was opened successfully, which means their credentials were valid. Return the AuthenticationInfo: | |
return createAuthenticationInfo(token, principal, credentials, ctx); | |
} finally { | |
LdapUtils.closeContext(ctx); | |
} | |
} | |
/** | |
* Returns the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. | |
* <p/> | |
* This implementation ignores the {@code ldapPrincipal}, {@code ldapCredentials}, and the opened | |
* {@code ldapContext} arguments and merely returns an {@code AuthenticationInfo} instance mirroring the | |
* submitted token's principal and credentials. This is acceptable because this method is only ever invoked after | |
* a successful authentication attempt, which means the provided principal and credentials were correct, and can | |
* be used directly to populate the (now verified) {@code AuthenticationInfo}. | |
* <p/> | |
* Subclasses however are free to override this method for more advanced construction logic. | |
* | |
* @param token the submitted {@code AuthenticationToken} that resulted in a successful authentication | |
* @param ldapPrincipal the LDAP principal used when creating the LDAP connection. Unlike the token's | |
* {@link AuthenticationToken#getPrincipal() principal}, this value is usually a constructed | |
* User DN and not a simple username or uid. The exact value is depending on the | |
* configured | |
* <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> | |
* LDAP authentication mechanism</a> in use. | |
* @param ldapCredentials the LDAP credentials used when creating the LDAP connection. | |
* @param ldapContext the LdapContext created that resulted in a successful authentication. It can be used | |
* further by subclasses for more complex operations. It does not need to be closed - | |
* it will be closed automatically after this method returns. | |
* @return the {@link AuthenticationInfo} resulting from a Subject's successful LDAP authentication attempt. | |
* @throws NamingException if there was any problem using the {@code LdapContext} | |
*/ | |
@SuppressWarnings({"UnusedDeclaration"}) | |
protected AuthenticationInfo createAuthenticationInfo(AuthenticationToken token, Object ldapPrincipal, | |
Object ldapCredentials, LdapContext ldapContext) | |
throws NamingException { | |
return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName()); | |
} | |
/** | |
* Method that should be implemented by subclasses to build an | |
* {@link AuthorizationInfo} object by querying the LDAP context for the | |
* specified principal.</p> | |
* | |
* @param principals the principals of the Subject whose AuthenticationInfo should be queried from the LDAP server. | |
* @param ldapContextFactory factory used to retrieve LDAP connections. | |
* @return an {@link AuthorizationInfo} instance containing information retrieved from the LDAP server. | |
* @throws NamingException if any LDAP errors occur during the search. | |
*/ | |
protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, | |
LdapContextFactory ldapContextFactory) throws NamingException { | |
return null; | |
} | |
} |