SEC-1181: Basic AuthenticationProvider for Active Directory.

This commit is contained in:
Luke Taylor 2011-03-06 22:13:32 +00:00
parent 4dc5d7d16e
commit 530f686149
5 changed files with 389 additions and 153 deletions

View File

@ -195,47 +195,55 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
return (DirContextOperations) executeReadOnly(new ContextExecutor() {
public Object executeWithContext(DirContext ctx) throws NamingException {
final DistinguishedName ctxBaseDn = new DistinguishedName(ctx.getNameInNamespace());
final DistinguishedName searchBaseDn = new DistinguishedName(base);
final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, params, searchControls);
if (logger.isDebugEnabled()) {
logger.debug("Searching for entry under DN '" + ctxBaseDn
+ "', base = '" + searchBaseDn + "', filter = '" + filter + "'");
}
Set<DirContextOperations> results = new HashSet<DirContextOperations>();
try {
while (resultsEnum.hasMore()) {
SearchResult searchResult = resultsEnum.next();
// Work out the DN of the matched entry
DistinguishedName dn = new DistinguishedName(new CompositeName(searchResult.getName()));
if (base.length() > 0) {
dn.prepend(searchBaseDn);
}
if (logger.isDebugEnabled()) {
logger.debug("Found DN: " + dn);
}
results.add(new DirContextAdapter(searchResult.getAttributes(), dn, ctxBaseDn));
}
} catch (PartialResultException e) {
LdapUtils.closeEnumeration(resultsEnum);
logger.info("Ignoring PartialResultException");
}
if (results.size() == 0) {
throw new IncorrectResultSizeDataAccessException(1, 0);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, results.size());
}
return results.toArray()[0];
return searchForSingleEntryInternal(ctx, searchControls, base, filter, params);
}
});
});
}
/**
* Internal method extracted to avoid code duplication in AD search.
*/
public static DirContextOperations searchForSingleEntryInternal(DirContext ctx, SearchControls searchControls,
String base, String filter, Object[] params) throws NamingException {
final DistinguishedName ctxBaseDn = new DistinguishedName(ctx.getNameInNamespace());
final DistinguishedName searchBaseDn = new DistinguishedName(base);
final NamingEnumeration<SearchResult> resultsEnum = ctx.search(searchBaseDn, filter, params, searchControls);
if (logger.isDebugEnabled()) {
logger.debug("Searching for entry under DN '" + ctxBaseDn
+ "', base = '" + searchBaseDn + "', filter = '" + filter + "'");
}
Set<DirContextOperations> results = new HashSet<DirContextOperations>();
try {
while (resultsEnum.hasMore()) {
SearchResult searchResult = resultsEnum.next();
// Work out the DN of the matched entry
DistinguishedName dn = new DistinguishedName(new CompositeName(searchResult.getName()));
if (base.length() > 0) {
dn.prepend(searchBaseDn);
}
if (logger.isDebugEnabled()) {
logger.debug("Found DN: " + dn);
}
results.add(new DirContextAdapter(searchResult.getAttributes(), dn, ctxBaseDn));
}
} catch (PartialResultException e) {
LdapUtils.closeEnumeration(resultsEnum);
logger.info("Ignoring PartialResultException");
}
if (results.size() == 0) {
throw new IncorrectResultSizeDataAccessException(1, 0);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, results.size());
}
return results.iterator().next();
}
/**

View File

@ -0,0 +1,134 @@
package org.springframework.security.ldap.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* Base class for the standard {@code LdapAuthenticationProvider} and the
* {@code ActiveDirectoryLdapAuthenticationProvider}.
*
* @author Luke Taylor
* @since 3.1
*/
public abstract class AbstractLdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
protected final Log logger = LogFactory.getLog(LdapAuthenticationProvider.class);
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private boolean useAuthenticationRequestCredentials = true;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
protected UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("LdapAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
final UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
String username = userToken.getName();
String password = (String) authentication.getCredentials();
if (logger.isDebugEnabled()) {
logger.debug("Processing authentication request for user: " + username);
}
if (!StringUtils.hasLength(username)) {
throw new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.emptyUsername",
"Empty Username"));
}
Assert.notNull(password, "Null password was supplied in authentication token");
DirContextOperations userData = doAuthentication(userToken);
UserDetails user = userDetailsContextMapper.mapUserFromContext(userData, authentication.getName(),
loadUserAuthorities(userData, authentication.getName(), (String)authentication.getCredentials()));
return createSuccessfulAuthentication(userToken, user);
}
protected abstract DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth);
protected abstract Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password);
/**
* Creates the final {@code Authentication} object which will be returned from the {@code authenticate} method.
*
* @param authentication the original authentication request token
* @param user the <tt>UserDetails</tt> instance returned by the configured <tt>UserDetailsContextMapper</tt>.
* @return the Authentication object for the fully authenticated user.
*/
protected Authentication createSuccessfulAuthentication(UsernamePasswordAuthenticationToken authentication,
UserDetails user) {
Object password = useAuthenticationRequestCredentials ? authentication.getCredentials() : user.getPassword();
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, password,
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Determines whether the supplied password will be used as the credentials in the successful authentication
* token. If set to false, then the password will be obtained from the UserDetails object
* created by the configured {@code UserDetailsContextMapper}.
* Often it will not be possible to read the password from the directory, so defaults to true.
*
* @param useAuthenticationRequestCredentials
*/
public void setUseAuthenticationRequestCredentials(boolean useAuthenticationRequestCredentials) {
this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
/**
* Allows a custom strategy to be used for creating the <tt>UserDetails</tt> which will be stored as the principal
* in the <tt>Authentication</tt> returned by the
* {@link #createSuccessfulAuthentication(org.springframework.security.authentication.UsernamePasswordAuthenticationToken, org.springframework.security.core.userdetails.UserDetails)} method.
*
* @param userDetailsContextMapper the strategy instance. If not set, defaults to a simple
* <tt>LdapUserDetailsMapper</tt>.
*/
public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) {
Assert.notNull(userDetailsContextMapper, "UserDetailsContextMapper must not be null");
this.userDetailsContextMapper = userDetailsContextMapper;
}
/**
* Provides access to the injected {@code UserDetailsContextMapper} strategy for use by subclasses.
*/
protected UserDetailsContextMapper getUserDetailsContextMapper() {
return userDetailsContextMapper;
}
}

View File

@ -15,27 +15,13 @@
package org.springframework.security.ldap.authentication;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.ppolicy.PasswordPolicyException;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
@ -43,7 +29,8 @@ import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.*;
/**
@ -128,21 +115,12 @@ import org.springframework.util.StringUtils;
* @see BindAuthenticator
* @see DefaultLdapAuthoritiesPopulator
*/
public class LdapAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
//~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(LdapAuthenticationProvider.class);
public class LdapAuthenticationProvider extends AbstractLdapAuthenticationProvider {
//~ Instance fields ================================================================================================
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private LdapAuthenticator authenticator;
private LdapAuthoritiesPopulator authoritiesPopulator;
private UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();
private boolean useAuthenticationRequestCredentials = true;
private boolean hideUserNotFoundExceptions = true;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
//~ Constructors ===================================================================================================
@ -190,78 +168,14 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Messa
return authoritiesPopulator;
}
/**
* Allows a custom strategy to be used for creating the <tt>UserDetails</tt> which will be stored as the principal
* in the <tt>Authentication</tt> returned by the
* {@link #createSuccessfulAuthentication(UsernamePasswordAuthenticationToken, UserDetails)} method.
*
* @param userDetailsContextMapper the strategy instance. If not set, defaults to a simple
* <tt>LdapUserDetailsMapper</tt>.
*/
public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) {
Assert.notNull(userDetailsContextMapper, "UserDetailsContextMapper must not be null");
this.userDetailsContextMapper = userDetailsContextMapper;
}
/**
* Provides access to the injected {@code UserDetailsContextMapper} strategy for use by subclasses.
*/
protected UserDetailsContextMapper getUserDetailsContextMapper() {
return userDetailsContextMapper;
}
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
/**
* Determines whether the supplied password will be used as the credentials in the successful authentication
* token. If set to false, then the password will be obtained from the UserDetails object
* created by the configured {@code UserDetailsContextMapper}.
* Often it will not be possible to read the password from the directory, so defaults to true.
*
* @param useAuthenticationRequestCredentials
*/
public void setUseAuthenticationRequestCredentials(boolean useAuthenticationRequestCredentials) {
this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("LdapAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
final UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
String username = userToken.getName();
String password = (String) authentication.getCredentials();
if (logger.isDebugEnabled()) {
logger.debug("Processing authentication request for user: " + username);
}
if (!StringUtils.hasLength(username)) {
throw new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.emptyUsername",
"Empty Username"));
}
Assert.notNull(password, "Null password was supplied in authentication token");
@Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken authentication) {
try {
DirContextOperations userData = getAuthenticator().authenticate(authentication);
UserDetails user = userDetailsContextMapper.mapUserFromContext(userData, username,
loadUserAuthorities(userData, username, password));
return createSuccessfulAuthentication(userToken, user);
return getAuthenticator().authenticate(authentication);
} catch (PasswordPolicyException ppe) {
// The only reason a ppolicy exception can occur during a bind is that the account is locked.
throw new LockedException(messages.getMessage(ppe.getStatus().getErrorCode(),
@ -278,30 +192,10 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Messa
}
}
@Override
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
return getAuthoritiesPopulator().getGrantedAuthorities(userData, username);
}
/**
* Creates the final {@code Authentication} object which will be returned from the {@code authenticate} method.
*
* @param authentication the original authentication request token
* @param user the <tt>UserDetails</tt> instance returned by the configured <tt>UserDetailsContextMapper</tt>.
* @return the Authentication object for the fully authenticated user.
*/
protected Authentication createSuccessfulAuthentication(UsernamePasswordAuthenticationToken authentication,
UserDetails user) {
Object password = useAuthenticationRequestCredentials ? authentication.getCredentials() : user.getPassword();
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, password,
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}

View File

@ -0,0 +1,175 @@
package org.springframework.security.ldap.authentication.ad;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.LdapUtils;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.*;
/**
* Specialized LDAP authentication provider which uses Active Directory configuration conventions.
* <p>
* It will authenticate using the Active Directory {@code userPrincipalName} (in the form {@code username@domain}).
* If the {@code usernameIncludesDomain} property is set to {@code true}, it is assumed that the user types in the
* full value, including the domain. Otherwise (the default), the {@code userPrincipalName} will be built from the
* username supplied in the authentication request.
* <p>
* The user authorities are obtained from the data contained in the {@code memberOf} attribute.
*
* @author Luke Taylor
* @since 3.1
*/
public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider {
private final String domain;
private final String rootDn;
private final String url;
private boolean usernameIncludesDomain = false;
/**
* @param domain the domain for which authentication should take place
*/
// public ActiveDirectoryLdapAuthenticationProvider(String domain) {
// this (domain, null);
// }
/**
* @param domain the domain name
* @param url an LDAP url (or multiple URLs)
*/
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {
Assert.isTrue(StringUtils.hasText(domain) || StringUtils.hasText(url), "Domain and url cannot both be empty");
this.domain = StringUtils.hasText(domain) ? domain : null;
this.url = StringUtils.hasText(url) ? url : null;
rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
}
@Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String)auth.getCredentials();
LdapContext ctx = bindAsUser(username, password);
try {
return searchForUser(ctx, username);
} catch (NamingException e) {
logger.error("Failed to locate directory entry for authentication user: " + username, e);
throw authenticationFailure();
} finally {
LdapUtils.closeContext(ctx);
}
}
/**
* Creates the user authority list from the values of the {@code memberOf} attribute obtained from the user's
* Active Directory entry.
*/
@Override
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
String[] groups = userData.getStringAttributes("memberOf");
if (groups == null) {
logger.debug("No values for 'memberOf' attribute.");
return AuthorityUtils.NO_AUTHORITIES;
}
if (logger.isDebugEnabled()) {
logger.debug("'memberOf' attribute values: " + Arrays.asList(groups));
}
ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(groups.length);
for (String group : groups) {
authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue()));
}
return authorities;
}
private LdapContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain
final String bindUrl = url;
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, createBindPrincipal(username));
env.put(Context.PROVIDER_URL, bindUrl);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
try {
return new InitialLdapContext(env, null);
} catch (NamingException e) {
logger.debug("Authentication failed", e);
throw authenticationFailure();
}
}
private BadCredentialsException authenticationFailure() {
return new BadCredentialsException(messages.getMessage(
"LdapAuthenticationProvider.badCredentials", "Bad credentials"));
}
private DirContextOperations searchForUser(LdapContext ctx, String username) throws NamingException {
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
final String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
new Object[]{bindPrincipal});
}
private String searchRootFromPrincipal(String bindPrincipal) {
return rootDnFromDomain(bindPrincipal.substring(bindPrincipal.lastIndexOf('@') + 1, bindPrincipal.length()));
}
private String rootDnFromDomain(String domain) {
String[] tokens = StringUtils.tokenizeToStringArray(domain, ".");
StringBuilder root = new StringBuilder();
for (String token : tokens) {
if (root.length() > 0) {
root.append(',');
}
root.append("dc=").append(token);
}
return root.toString();
}
private String createBindPrincipal(String username) {
if (usernameIncludesDomain || domain == null) {
return username;
}
return username + "@" + domain;
}
public void setUsernameIncludesDomain(boolean usernameIncludesDomain) {
Assert.isTrue(domain != null || usernameIncludesDomain,
"If the domain name is not included in the username, a domain must be set in the constructor");
this.usernameIncludesDomain = usernameIncludesDomain;
}
}

View File

@ -0,0 +1,25 @@
package org.springframework.security.ldap.authentication.ad;
import static org.junit.Assert.*;
import org.junit.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* @author Luke Taylor
*/
public class ActiveDirectoryLdapAuthenticationProviderTests {
@Test
public void simpleAuthenticationWithIsSucessful() throws Exception {
ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider(null, "ldap://192.168.1.200/");
Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("luke@fenetres.monkeymachine.eu","p!ssw0rd"));
assertEquals(1, result.getAuthorities().size());
assertTrue(result.getAuthorities().contains(new SimpleGrantedAuthority("blah")));
}
}