SEC-617: Make LDAPAuthenticationProvider a standalone class.

This commit is contained in:
Luke Taylor 2007-12-05 14:39:46 +00:00
parent 88ab9671c6
commit 22052115b6
2 changed files with 67 additions and 77 deletions

View File

@ -15,58 +15,57 @@
package org.springframework.security.providers.ldap; package org.springframework.security.providers.ldap;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException; import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationServiceException;
import org.springframework.security.BadCredentialsException; import org.springframework.security.BadCredentialsException;
import org.springframework.security.GrantedAuthority; import org.springframework.security.GrantedAuthority;
import org.springframework.security.AuthenticationServiceException; import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.security.providers.AuthenticationProvider;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken; import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.providers.ldap.authenticator.LdapShaPasswordEncoder;
import org.springframework.security.providers.encoding.PasswordEncoder;
import org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.ldap.UserDetailsContextMapper;
import org.springframework.security.userdetails.ldap.LdapUserDetailsMapper; import org.springframework.security.userdetails.ldap.LdapUserDetailsMapper;
import org.springframework.security.userdetails.ldap.UserDetailsContextMapper;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.ldap.core.DirContextOperations;
/** /**
* An {@link org.springframework.security.providers.AuthenticationProvider} implementation that provides integration with an * An {@link org.springframework.security.providers.AuthenticationProvider} implementation that provides integration
* LDAP server. * with an LDAP server.
* * <p>
* <p>There are many ways in which an LDAP directory can be configured so this class delegates most of * There are many ways in which an LDAP directory can be configured so this class delegates most of
* its responsibilites to two separate strategy interfaces, {@link LdapAuthenticator} * its responsibilites to two separate strategy interfaces, {@link LdapAuthenticator}
* and {@link LdapAuthoritiesPopulator}.</p> * and {@link LdapAuthoritiesPopulator}.
* *
* <h3>LdapAuthenticator</h3> * <h3>LdapAuthenticator</h3>
* This interface is responsible for performing the user authentication and retrieving * This interface is responsible for performing the user authentication and retrieving
* the user's information from the directory. Example implementations are {@link * the user's information from the directory. Example implementations are {@link
* org.springframework.security.providers.ldap.authenticator.BindAuthenticator BindAuthenticator} which authenticates the user by * org.springframework.security.providers.ldap.authenticator.BindAuthenticator BindAuthenticator} which authenticates
* "binding" as that user, and {@link org.springframework.security.providers.ldap.authenticator.PasswordComparisonAuthenticator * the user by "binding" as that user, and
* PasswordComparisonAuthenticator} which performs a comparison of the supplied password with the value stored in the * {@link org.springframework.security.providers.ldap.authenticator.PasswordComparisonAuthenticator PasswordComparisonAuthenticator}
* directory, either by retrieving the password or performing an LDAP "compare" operation. * which compares the supplied password with the value stored in the directory, using an LDAP "compare"
* <p>The task of retrieving the user attributes is delegated to the authenticator because the permissions on the * operation.
* <p>
* The task of retrieving the user attributes is delegated to the authenticator because the permissions on the
* attributes may depend on the type of authentication being used; for example, if binding as the user, it may be * attributes may depend on the type of authentication being used; for example, if binding as the user, it may be
* necessary to read them with the user's own permissions (using the same context used for the bind operation).</p> * necessary to read them with the user's own permissions (using the same context used for the bind operation).
* *
* <h3>LdapAuthoritiesPopulator</h3> * <h3>LdapAuthoritiesPopulator</h3>
* Once the user has been authenticated, this interface is called to obtain the set of granted authorities for the * Once the user has been authenticated, this interface is called to obtain the set of granted authorities for the
* user. * user.
* The * The {@link org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator DefaultLdapAuthoritiesPopulator}
* {@link org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator DefaultLdapAuthoritiesPopulator}
* can be configured to obtain user role information from the user's attributes and/or to perform a search for * can be configured to obtain user role information from the user's attributes and/or to perform a search for
* "groups" that the user is a member of and map these to roles. * "groups" that the user is a member of and map these to roles.
* *
* <p>A custom implementation could obtain the roles from a completely different source, for example from a database. * <p>
* </p> * A custom implementation could obtain the roles from a completely different source, for example from a database.
* *
* <h3>Configuration</h3> * <h3>Configuration</h3>
* *
@ -94,21 +93,22 @@ import org.springframework.ldap.core.DirContextOperations;
* &lt;/constructor-arg> * &lt;/constructor-arg>
* &lt;/bean></pre> * &lt;/bean></pre>
* *
* <p>This would set up the provider to access an LDAP server with URL * <p>
* This would set up the provider to access an LDAP server with URL
* <tt>ldap://monkeymachine:389/dc=springframework,dc=org</tt>. Authentication will be performed by attempting to bind * <tt>ldap://monkeymachine:389/dc=springframework,dc=org</tt>. Authentication will be performed by attempting to bind
* with the DN <tt>uid=&lt;user-login-name&gt;,ou=people,dc=springframework,dc=org</tt>. After successful * with the DN <tt>uid=&lt;user-login-name&gt;,ou=people,dc=springframework,dc=org</tt>. After successful
* authentication, roles will be assigned to the user by searching under the DN * authentication, roles will be assigned to the user by searching under the DN
* <tt>ou=groups,dc=springframework,dc=org</tt> with the default filter <tt>(member=&lt;user's-DN&gt;)</tt>. The role * <tt>ou=groups,dc=springframework,dc=org</tt> with the default filter <tt>(member=&lt;user's-DN&gt;)</tt>. The role
* name will be taken from the "ou" attribute of each match.</p> * name will be taken from the "ou" attribute of each match.
* <p> * <p>
* The authenticate method will reject empty passwords outright. LDAP servers may allow an anonymous * The authenticate method will reject empty passwords outright. LDAP servers may allow an anonymous
* bind operation with an empty password, even if a DN is supplied. In practice this means that if * bind operation with an empty password, even if a DN is supplied. In practice this means that if
* the LDAP directory is configured to allow unauthenitcated access, it might be possible to * the LDAP directory is configured to allow unauthenticated access, it might be possible to
* authenticate as <i>any</i> user just by supplying an empty password. * authenticate as <i>any</i> user just by supplying an empty password.
* More information on the misuse of unauthenticated access can be found in * More information on the misuse of unauthenticated access can be found in
* <a href="http://www.ietf.org/internet-drafts/draft-ietf-ldapbis-authmeth-19.txt"> * <a href="http://www.ietf.org/internet-drafts/draft-ietf-ldapbis-authmeth-19.txt">
* draft-ietf-ldapbis-authmeth-19.txt</a>. * draft-ietf-ldapbis-authmeth-19.txt</a>.
* </p> *
* *
* @author Luke Taylor * @author Luke Taylor
* @version $Id$ * @version $Id$
@ -116,18 +116,18 @@ import org.springframework.ldap.core.DirContextOperations;
* @see org.springframework.security.providers.ldap.authenticator.BindAuthenticator * @see org.springframework.security.providers.ldap.authenticator.BindAuthenticator
* @see org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator * @see org.springframework.security.providers.ldap.populator.DefaultLdapAuthoritiesPopulator
*/ */
public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { public class LdapAuthenticationProvider implements AuthenticationProvider {
//~ Static fields/initializers ===================================================================================== //~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(LdapAuthenticationProvider.class); private static final Log logger = LogFactory.getLog(LdapAuthenticationProvider.class);
//~ Instance fields ================================================================================================ //~ Instance fields ================================================================================================
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private LdapAuthenticator authenticator; private LdapAuthenticator authenticator;
private LdapAuthoritiesPopulator authoritiesPopulator; private LdapAuthoritiesPopulator authoritiesPopulator;
private UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper(); private UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();
private PasswordEncoder passwordEncoder = new LdapShaPasswordEncoder();
private boolean includeDetailsObject = true;
//~ Constructors =================================================================================================== //~ Constructors ===================================================================================================
@ -184,29 +184,20 @@ public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticatio
return userDetailsContextMapper; return userDetailsContextMapper;
} }
protected void additionalAuthenticationChecks(UserDetails userDetails, public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authentication) Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
throws AuthenticationException { messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
String presentedPassword = authentication.getCredentials() == null ? "" : "Only UsernamePasswordAuthenticationToken is supported"));
authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, null)) { UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken)authentication;
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), String username = userToken.getName();
includeDetailsObject ? userDetails : null);
}
}
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (!StringUtils.hasLength(username)) { if (!StringUtils.hasLength(username)) {
throw new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.emptyUsername", throw new BadCredentialsException(messages.getMessage("LdapAuthenticationProvider.emptyUsername",
"Empty Username")); "Empty Username"));
} }
if (logger.isDebugEnabled()) {
logger.debug("Retrieving user " + username);
}
String password = (String) authentication.getCredentials(); String password = (String) authentication.getCredentials();
Assert.notNull(password, "Null password was supplied in authentication token"); Assert.notNull(password, "Null password was supplied in authentication token");
@ -217,23 +208,27 @@ public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticatio
} }
try { try {
DirContextOperations user = getAuthenticator().authenticate(authentication); DirContextOperations userData = getAuthenticator().authenticate(authentication);
GrantedAuthority[] extraAuthorities = getAuthoritiesPopulator().getGrantedAuthorities(user, username); GrantedAuthority[] extraAuthorities = getAuthoritiesPopulator().getGrantedAuthorities(userData, username);
return userDetailsContextMapper.mapUserFromContext(user, username, extraAuthorities); UserDetails user = userDetailsContextMapper.mapUserFromContext(userData, username, extraAuthorities);
} catch (DataAccessException ldapAccessFailure) { return createSuccessfulAuthentication(userToken, user);
} catch (NamingException ldapAccessFailure) {
throw new AuthenticationServiceException(ldapAccessFailure.getMessage(), ldapAccessFailure); throw new AuthenticationServiceException(ldapAccessFailure.getMessage(), ldapAccessFailure);
} }
} }
public boolean isIncludeDetailsObject() { protected Authentication createSuccessfulAuthentication(UsernamePasswordAuthenticationToken authentication,
return includeDetailsObject; UserDetails user) {
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
} }
public void setIncludeDetailsObject(boolean includeDetailsObject) { public boolean supports(Class authentication) {
this.includeDetailsObject = includeDetailsObject; return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
} }
//~ Inner Classes ================================================================================================== //~ Inner Classes ==================================================================================================

View File

@ -51,22 +51,19 @@ public class LdapAuthenticationProviderTests extends TestCase {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
public void testDifferentCacheValueCausesException() {
public void testSupportsUsernamePasswordAuthenticationToken() {
LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator(), LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator(),
new MockAuthoritiesPopulator()); new MockAuthoritiesPopulator());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", "benspassword");
// User is authenticated here assertTrue(ldapProvider.supports(UsernamePasswordAuthenticationToken.class));
UserDetails user = ldapProvider.retrieveUser("ben", authRequest); }
// Assume the user details object is cached...
// And a subsequent authentication request comes in on the cached data public void testDefaultMapperIsSet() {
authRequest = new UsernamePasswordAuthenticationToken("ben", "wrongpassword"); LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator(),
new MockAuthoritiesPopulator());
try { assertTrue(ldapProvider.getUserDetailsContextMapper() instanceof LdapUserDetailsMapper);
ldapProvider.additionalAuthenticationChecks(user, authRequest);
fail("Expected BadCredentialsException should have failed with wrong password");
} catch (BadCredentialsException expected) {}
} }
public void testEmptyOrNullUserNameThrowsException() { public void testEmptyOrNullUserNameThrowsException() {
@ -74,12 +71,12 @@ public class LdapAuthenticationProviderTests extends TestCase {
new MockAuthoritiesPopulator()); new MockAuthoritiesPopulator());
try { try {
ldapProvider.retrieveUser("", new UsernamePasswordAuthenticationToken("bob", "bobspassword")); ldapProvider.authenticate(new UsernamePasswordAuthenticationToken(null, "password"));
fail("Expected BadCredentialsException for empty username"); fail("Expected BadCredentialsException for empty username");
} catch (BadCredentialsException expected) {} } catch (BadCredentialsException expected) {}
try { try {
ldapProvider.retrieveUser(null, new UsernamePasswordAuthenticationToken("bob", "bobspassword")); ldapProvider.authenticate(new UsernamePasswordAuthenticationToken("", "bobspassword"));
fail("Expected BadCredentialsException for null username"); fail("Expected BadCredentialsException for null username");
} catch (BadCredentialsException expected) {} } catch (BadCredentialsException expected) {}
} }
@ -87,7 +84,7 @@ public class LdapAuthenticationProviderTests extends TestCase {
public void testEmptyPasswordIsRejected() { public void testEmptyPasswordIsRejected() {
LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator()); LdapAuthenticationProvider ldapProvider = new LdapAuthenticationProvider(new MockAuthenticator());
try { try {
ldapProvider.retrieveUser("jen", new UsernamePasswordAuthenticationToken("jen", "")); ldapProvider.authenticate(new UsernamePasswordAuthenticationToken("jen", ""));
fail("Expected BadCredentialsException for empty password"); fail("Expected BadCredentialsException for empty password");
} catch (BadCredentialsException expected) {} } catch (BadCredentialsException expected) {}
} }
@ -102,7 +99,7 @@ public class LdapAuthenticationProviderTests extends TestCase {
assertNotNull(ldapProvider.getAuthoritiesPopulator()); assertNotNull(ldapProvider.getAuthoritiesPopulator());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", "benspassword"); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", "benspassword");
UserDetails user = ldapProvider.retrieveUser("ben", authRequest); UserDetails user = (UserDetails) ldapProvider.authenticate(authRequest).getPrincipal();
assertEquals(2, user.getAuthorities().length); assertEquals(2, user.getAuthorities().length);
assertEquals("{SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=", user.getPassword()); assertEquals("{SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=", user.getPassword());
assertEquals("ben", user.getUsername()); assertEquals("ben", user.getUsername());
@ -113,8 +110,6 @@ public class LdapAuthenticationProviderTests extends TestCase {
assertTrue(authorities.contains("ROLE_FROM_ENTRY")); assertTrue(authorities.contains("ROLE_FROM_ENTRY"));
assertTrue(authorities.contains("ROLE_FROM_POPULATOR")); assertTrue(authorities.contains("ROLE_FROM_POPULATOR"));
ldapProvider.additionalAuthenticationChecks(user, authRequest);
} }
public void testUseWithNullAuthoritiesPopulatorReturnsCorrectRole() { public void testUseWithNullAuthoritiesPopulatorReturnsCorrectRole() {
@ -123,7 +118,7 @@ public class LdapAuthenticationProviderTests extends TestCase {
userMapper.setRoleAttributes(new String[] {"ou"}); userMapper.setRoleAttributes(new String[] {"ou"});
ldapProvider.setUserDetailsContextMapper(userMapper); ldapProvider.setUserDetailsContextMapper(userMapper);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", "benspassword"); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("ben", "benspassword");
UserDetails user = ldapProvider.retrieveUser("ben", authRequest); UserDetails user = (UserDetails) ldapProvider.authenticate(authRequest).getPrincipal();
assertEquals(1, user.getAuthorities().length); assertEquals(1, user.getAuthorities().length);
assertEquals("ROLE_FROM_ENTRY", user.getAuthorities()[0].getAuthority()); assertEquals("ROLE_FROM_ENTRY", user.getAuthorities()[0].getAuthority());
} }