SEC-536: Introduced UserDetailsChecker strategy to extract code for checking status of accounts and allowing variation in pre/post authentication checks made by AbstractUserDetailsAuthenticationProvider

This commit is contained in:
Luke Taylor 2008-02-15 18:05:12 +00:00
parent da90b81e16
commit 5e204e23f3
10 changed files with 84 additions and 114 deletions

View File

@ -5,7 +5,6 @@ import org.springframework.security.ui.preauth.x509.X509PreAuthenticatedProcessi
import org.springframework.security.ui.preauth.x509.SubjectDnX509PrincipalExtractor; import org.springframework.security.ui.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.providers.preauth.UserDetailsByNameServiceWrapper; import org.springframework.security.providers.preauth.UserDetailsByNameServiceWrapper;
import org.springframework.security.userdetails.decorator.StatusCheckingUserDetailsService;
import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@ -52,12 +51,9 @@ public class X509BeanDefinitionParser implements BeanDefinitionParser {
String userServiceRef = element.getAttribute(ATT_USER_SERVICE_REF); String userServiceRef = element.getAttribute(ATT_USER_SERVICE_REF);
if (StringUtils.hasText(userServiceRef)) { if (StringUtils.hasText(userServiceRef)) {
RootBeanDefinition statusCheckingUserService = new RootBeanDefinition(StatusCheckingUserDetailsService.class);
statusCheckingUserService.setSource(source);
statusCheckingUserService.getConstructorArgumentValues().addIndexedArgumentValue(0, new RuntimeBeanReference(userServiceRef));
RootBeanDefinition preAuthUserService = new RootBeanDefinition(UserDetailsByNameServiceWrapper.class); RootBeanDefinition preAuthUserService = new RootBeanDefinition(UserDetailsByNameServiceWrapper.class);
preAuthUserService.setSource(source); preAuthUserService.setSource(source);
preAuthUserService.getPropertyValues().addPropertyValue("userDetailsService", statusCheckingUserService); preAuthUserService.getPropertyValues().addPropertyValue("userDetailsService", new RuntimeBeanReference(userServiceRef));
provider.getPropertyValues().addPropertyValue("preAuthenticatedUserDetailsService", preAuthUserService); provider.getPropertyValues().addPropertyValue("preAuthenticatedUserDetailsService", preAuthUserService);
} }

View File

@ -31,6 +31,7 @@ import org.springframework.security.providers.dao.cache.NullUserCache;
import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException; import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.security.userdetails.UserDetailsChecker;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
@ -56,8 +57,8 @@ import org.springframework.util.Assert;
* and <code>UserDetails</code> implementations provide additional flexibility, by default a <code>UserDetails</code> * and <code>UserDetails</code> implementations provide additional flexibility, by default a <code>UserDetails</code>
* is returned. To override this * is returned. To override this
* default, set the {@link #setForcePrincipalAsString} to <code>true</code>. * default, set the {@link #setForcePrincipalAsString} to <code>true</code>.
* </p> * <p>
* <p>Caching is handled via the <code>UserDetails</code> object being placed in the {@link UserCache}. This * Caching is handled via the <code>UserDetails</code> object being placed in the {@link UserCache}. This
* ensures that subsequent requests with the same username can be validated without needing to query the {@link * ensures that subsequent requests with the same username can be validated without needing to query the {@link
* UserDetailsService}. It should be noted that if a user appears to present an incorrect password, the {@link * UserDetailsService}. It should be noted that if a user appears to present an incorrect password, the {@link
* UserDetailsService} will be queried to confirm the most up-to-date password was used for comparison.</p> * UserDetailsService} will be queried to confirm the most up-to-date password was used for comparison.</p>
@ -66,13 +67,15 @@ import org.springframework.util.Assert;
* @version $Id$ * @version $Id$
*/ */
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean,
MessageSourceAware { MessageSourceAware {
//~ Instance fields ================================================================================================ //~ Instance fields ================================================================================================
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache(); private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false; private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true; protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
@ -100,8 +103,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
doAfterPropertiesSet(); doAfterPropertiesSet();
} }
public Authentication authenticate(Authentication authentication) public Authentication authenticate(Authentication authentication) throws AuthenticationException {
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported")); "Only UsernamePasswordAuthenticationToken is supported"));
@ -129,20 +131,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
} }
if (!user.isAccountNonLocked()) { preAuthenticationChecks.check(user);
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
// This check must come here, as we don't want to tell users // This check must come here, as we don't want to tell users
// about account status unless they presented the correct credentials // about account status unless they presented the correct credentials
@ -160,10 +149,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
} }
} }
if (!user.isCredentialsNonExpired()) { postAuthenticationChecks.check(user);
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
if (!cacheWasUsed) { if (!cacheWasUsed) {
this.userCache.putUserInCache(user); this.userCache.putUserInCache(user);
@ -278,4 +264,50 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
public boolean supports(Class authentication) { public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
} }
protected UserDetailsChecker getPreAuthenticationChecks() {
return preAuthenticationChecks;
}
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
this.preAuthenticationChecks = preAuthenticationChecks;
}
protected UserDetailsChecker getPostAuthenticationChecks() {
return postAuthenticationChecks;
}
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
this.postAuthenticationChecks = postAuthenticationChecks;
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
}
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
}
}
} }

View File

@ -15,7 +15,8 @@ import org.springframework.security.ui.logout.LogoutHandler;
import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException; import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.security.userdetails.decorator.StatusCheckingUserDetailsService; import org.springframework.security.userdetails.UserDetailsChecker;
import org.springframework.security.userdetails.checker.AccountStatusUserDetailsChecker;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.bind.ServletRequestUtils;
@ -44,8 +45,8 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService; private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl(); private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY; private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
@ -83,6 +84,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
try { try {
String[] cookieTokens = decodeCookie(rememberMeCookie); String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response); user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
} catch (CookieTheftException cte) { } catch (CookieTheftException cte) {
cancelCookie(request, response); cancelCookie(request, response);
throw cte; throw cte;
@ -319,7 +321,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
} }
public void setUserDetailsService(UserDetailsService userDetailsService) { public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = new StatusCheckingUserDetailsService(userDetailsService); this.userDetailsService = userDetailsService;
} }
public void setKey(String key) { public void setKey(String key) {

View File

@ -35,8 +35,9 @@ import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.AbstractProcessingFilter; import org.springframework.security.ui.AbstractProcessingFilter;
import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.decorator.StatusCheckingUserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException; import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.security.userdetails.UserDetailsChecker;
import org.springframework.security.userdetails.checker.AccountStatusUserDetailsChecker;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -120,6 +121,7 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
private String switchFailureUrl; private String switchFailureUrl;
private SwitchUserAuthorityChanger switchUserAuthorityChanger; private SwitchUserAuthorityChanger switchUserAuthorityChanger;
private UserDetailsService userDetailsService; private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private boolean useRelativeContext; private boolean useRelativeContext;
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
@ -204,8 +206,8 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
logger.debug("Attempt to switch to user [" + username + "]"); logger.debug("Attempt to switch to user [" + username + "]");
} }
// load the user by name UserDetails targetUser = userDetailsService.loadUserByUsername(username);
UserDetails targetUser = this.userDetailsService.loadUserByUsername(username); userDetailsChecker.check(targetUser);
// ok, create the switch user token // ok, create the switch user token
targetUserRequest = createSwitchUserToken(request, targetUser); targetUserRequest = createSwitchUserToken(request, targetUser);
@ -426,7 +428,7 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
* @param userDetailsService The authentication dao * @param userDetailsService The authentication dao
*/ */
public void setUserDetailsService(UserDetailsService userDetailsService) { public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = new StatusCheckingUserDetailsService(userDetailsService); this.userDetailsService = userDetailsService;
} }
/** /**

View File

@ -0,0 +1,10 @@
package org.springframework.security.userdetails;
/**
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public interface UserDetailsChecker {
void check(UserDetails toCheck);
}

View File

@ -1,39 +1,23 @@
package org.springframework.security.userdetails.decorator; package org.springframework.security.userdetails.checker;
import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UserDetailsChecker;
import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.LockedException; import org.springframework.security.LockedException;
import org.springframework.security.DisabledException; import org.springframework.security.DisabledException;
import org.springframework.security.AccountExpiredException; import org.springframework.security.AccountExpiredException;
import org.springframework.security.CredentialsExpiredException; import org.springframework.security.CredentialsExpiredException;
import org.springframework.security.SpringSecurityMessageSource; import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.security.AuthenticationException;
import org.springframework.dao.DataAccessException;
import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.util.Assert;
/** /**
* Decorates a {@link UserDetailsService}, making it throw an exception if the account is locked, disabled etc. This
* removes the need for separate account status checks in classes which make use of a <tt>UserDetailsService</tt>.
*
* @author Luke Taylor * @author Luke Taylor
* @version $Id$ * @version $Id$
*/ */
public class StatusCheckingUserDetailsService implements UserDetailsService { public class AccountStatusUserDetailsChecker implements UserDetailsChecker {
private UserDetailsService delegate;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
public StatusCheckingUserDetailsService(UserDetailsService userDetailsService) { public void check(UserDetails user) {
this.delegate = userDetailsService;
}
public UserDetails loadUserByUsername(String username) throws AuthenticationException, DataAccessException {
UserDetails user = delegate.loadUserByUsername(username);
Assert.notNull(user, "UserDetailsService returned null user, an interface violation.");
if (!user.isAccountNonLocked()) { if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage("UserDetailsService.locked", "User account is locked")); throw new LockedException(messages.getMessage("UserDetailsService.locked", "User account is locked"));
} }
@ -51,7 +35,5 @@ public class StatusCheckingUserDetailsService implements UserDetailsService {
throw new CredentialsExpiredException(messages.getMessage("UserDetailsService.credentialsExpired", throw new CredentialsExpiredException(messages.getMessage("UserDetailsService.credentialsExpired",
"User credentials have expired")); "User credentials have expired"));
} }
return user;
} }
} }

View File

@ -50,14 +50,6 @@ import org.springframework.dao.DataRetrievalFailureException;
public class SiteminderAuthenticationProviderTests extends TestCase { public class SiteminderAuthenticationProviderTests extends TestCase {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
public static void main(String[] args) {
junit.textui.TestRunner.run(SiteminderAuthenticationProviderTests.class);
}
public final void setUp() throws Exception {
super.setUp();
}
public void testAuthenticateFailsIfAccountExpired() { public void testAuthenticateFailsIfAccountExpired() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");

View File

@ -478,11 +478,11 @@ public class AbstractProcessingFilterTests extends TestCase {
// Setup our test object, to grant access // Setup our test object, to grant access
MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true); MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true);
filter.setDefaultTargetUrl("http://monkeymachine.co.uk/"); filter.setDefaultTargetUrl("https://monkeymachine.co.uk/");
filter.setAlwaysUseDefaultTargetUrl(true); filter.setAlwaysUseDefaultTargetUrl(true);
executeFilterInContainerSimulator(config, filter, request, response, chain); executeFilterInContainerSimulator(config, filter, request, response, chain);
assertEquals("http://monkeymachine.co.uk/", response.getRedirectedUrl()); assertEquals("https://monkeymachine.co.uk/", response.getRedirectedUrl());
assertNotNull(SecurityContextHolder.getContext().getAuthentication()); assertNotNull(SecurityContextHolder.getContext().getAuthentication());
} }

View File

@ -1,43 +0,0 @@
package org.springframework.security.userdetails.decorator;
import org.springframework.security.userdetails.MockUserDetailsService;
import org.springframework.security.LockedException;
import org.springframework.security.DisabledException;
import org.springframework.security.CredentialsExpiredException;
import org.springframework.security.AccountExpiredException;
import org.junit.Test;
/**
* @author Luke Taylor
* @version $Id$
*/
public class StatusCheckingUserDetailsServiceTests {
private StatusCheckingUserDetailsService us = new StatusCheckingUserDetailsService(new MockUserDetailsService());
@Test
public void validAccountIsSuccessfullyLoaded() throws Exception {
us.loadUserByUsername("valid");
}
@Test(expected = LockedException.class)
public void lockedAccountThrowsLockedException() throws Exception {
us.loadUserByUsername("locked");
}
@Test(expected = DisabledException.class)
public void disabledAccountThrowsDisabledException() throws Exception {
us.loadUserByUsername("disabled");
}
@Test(expected = CredentialsExpiredException.class)
public void credentialsExpiredAccountThrowsCredentialsExpiredException() throws Exception {
us.loadUserByUsername("credentialsExpired");
}
@Test(expected = AccountExpiredException.class)
public void expiredAccountThrowsAccountExpiredException() throws Exception {
us.loadUserByUsername("expired");
}
}

View File

@ -34,9 +34,6 @@ import org.springframework.util.Assert;
* enabled/disabled status of the <code>UserDetails</code> because this is * enabled/disabled status of the <code>UserDetails</code> because this is
* authentication-related and should have been enforced by another provider server. * authentication-related and should have been enforced by another provider server.
* <p> * <p>
* You can optionally have these checked by configuring wrapping the <tt>UserDetailsService</tt> in a
* {@link org.springframework.security.userdetails.decorator.StatusCheckingUserDetailsService} decorator.
* <p>
* The <code>UserDetails</code> returned by implementations is stored in the generated <code>AuthenticationToken</code>, * The <code>UserDetails</code> returned by implementations is stored in the generated <code>AuthenticationToken</code>,
* so additional properties such as email addresses, telephone numbers etc can easily be stored. * so additional properties such as email addresses, telephone numbers etc can easily be stored.
* *