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:
parent
da90b81e16
commit
5e204e23f3
|
@ -5,7 +5,6 @@ import org.springframework.security.ui.preauth.x509.X509PreAuthenticatedProcessi
|
|||
import org.springframework.security.ui.preauth.x509.SubjectDnX509PrincipalExtractor;
|
||||
import org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationProvider;
|
||||
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.ParserContext;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
|
@ -52,12 +51,9 @@ public class X509BeanDefinitionParser implements BeanDefinitionParser {
|
|||
String userServiceRef = element.getAttribute(ATT_USER_SERVICE_REF);
|
||||
|
||||
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);
|
||||
preAuthUserService.setSource(source);
|
||||
preAuthUserService.getPropertyValues().addPropertyValue("userDetailsService", statusCheckingUserService);
|
||||
preAuthUserService.getPropertyValues().addPropertyValue("userDetailsService", new RuntimeBeanReference(userServiceRef));
|
||||
provider.getPropertyValues().addPropertyValue("preAuthenticatedUserDetailsService", preAuthUserService);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.springframework.security.providers.dao.cache.NullUserCache;
|
|||
import org.springframework.security.userdetails.UserDetails;
|
||||
import org.springframework.security.userdetails.UserDetailsService;
|
||||
import org.springframework.security.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.userdetails.UserDetailsChecker;
|
||||
|
||||
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>
|
||||
* is returned. To override this
|
||||
* default, set the {@link #setForcePrincipalAsString} to <code>true</code>.
|
||||
* </p>
|
||||
* <p>Caching is handled via the <code>UserDetails</code> object being placed in the {@link UserCache}. This
|
||||
* <p>
|
||||
* 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
|
||||
* 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>
|
||||
|
@ -66,13 +67,15 @@ import org.springframework.util.Assert;
|
|||
* @version $Id$
|
||||
*/
|
||||
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean,
|
||||
MessageSourceAware {
|
||||
MessageSourceAware {
|
||||
//~ Instance fields ================================================================================================
|
||||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
private UserCache userCache = new NullUserCache();
|
||||
private boolean forcePrincipalAsString = false;
|
||||
protected boolean hideUserNotFoundExceptions = true;
|
||||
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
|
||||
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
|
||||
|
||||
//~ Methods ========================================================================================================
|
||||
|
||||
|
@ -100,8 +103,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
|
|||
doAfterPropertiesSet();
|
||||
}
|
||||
|
||||
public Authentication authenticate(Authentication authentication)
|
||||
throws AuthenticationException {
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
|
||||
messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
|
||||
"Only UsernamePasswordAuthenticationToken is supported"));
|
||||
|
@ -129,21 +131,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
|
|||
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
preAuthenticationChecks.check(user);
|
||||
|
||||
// This check must come here, as we don't want to tell users
|
||||
// about account status unless they presented the correct credentials
|
||||
try {
|
||||
|
@ -160,10 +149,7 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
|
|||
}
|
||||
}
|
||||
|
||||
if (!user.isCredentialsNonExpired()) {
|
||||
throw new CredentialsExpiredException(messages.getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
|
||||
}
|
||||
postAuthenticationChecks.check(user);
|
||||
|
||||
if (!cacheWasUsed) {
|
||||
this.userCache.putUserInCache(user);
|
||||
|
@ -278,4 +264,50 @@ public abstract class AbstractUserDetailsAuthenticationProvider implements Authe
|
|||
public boolean supports(Class 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"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ import org.springframework.security.ui.logout.LogoutHandler;
|
|||
import org.springframework.security.userdetails.UserDetails;
|
||||
import org.springframework.security.userdetails.UserDetailsService;
|
||||
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.StringUtils;
|
||||
import org.springframework.web.bind.ServletRequestUtils;
|
||||
|
@ -44,8 +45,8 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
|
|||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
|
||||
private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
|
||||
|
||||
private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
|
||||
|
@ -83,6 +84,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
|
|||
try {
|
||||
String[] cookieTokens = decodeCookie(rememberMeCookie);
|
||||
user = processAutoLoginCookie(cookieTokens, request, response);
|
||||
userDetailsChecker.check(user);
|
||||
} catch (CookieTheftException cte) {
|
||||
cancelCookie(request, response);
|
||||
throw cte;
|
||||
|
@ -319,7 +321,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
|
|||
}
|
||||
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = new StatusCheckingUserDetailsService(userDetailsService);
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
|
|
|
@ -35,8 +35,9 @@ import org.springframework.security.ui.FilterChainOrder;
|
|||
import org.springframework.security.ui.AbstractProcessingFilter;
|
||||
import org.springframework.security.userdetails.UserDetails;
|
||||
import org.springframework.security.userdetails.UserDetailsService;
|
||||
import org.springframework.security.userdetails.decorator.StatusCheckingUserDetailsService;
|
||||
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.LogFactory;
|
||||
|
@ -120,6 +121,7 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
|
|||
private String switchFailureUrl;
|
||||
private SwitchUserAuthorityChanger switchUserAuthorityChanger;
|
||||
private UserDetailsService userDetailsService;
|
||||
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
|
||||
private boolean useRelativeContext;
|
||||
|
||||
//~ Methods ========================================================================================================
|
||||
|
@ -204,8 +206,8 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
|
|||
logger.debug("Attempt to switch to user [" + username + "]");
|
||||
}
|
||||
|
||||
// load the user by name
|
||||
UserDetails targetUser = this.userDetailsService.loadUserByUsername(username);
|
||||
UserDetails targetUser = userDetailsService.loadUserByUsername(username);
|
||||
userDetailsChecker.check(targetUser);
|
||||
|
||||
// ok, create the switch user token
|
||||
targetUserRequest = createSwitchUserToken(request, targetUser);
|
||||
|
@ -426,7 +428,7 @@ public class SwitchUserProcessingFilter extends SpringSecurityFilter implements
|
|||
* @param userDetailsService The authentication dao
|
||||
*/
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = new StatusCheckingUserDetailsService(userDetailsService);
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.springframework.security.userdetails;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface UserDetailsChecker {
|
||||
void check(UserDetails toCheck);
|
||||
}
|
|
@ -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.LockedException;
|
||||
import org.springframework.security.DisabledException;
|
||||
import org.springframework.security.AccountExpiredException;
|
||||
import org.springframework.security.CredentialsExpiredException;
|
||||
import org.springframework.security.SpringSecurityMessageSource;
|
||||
import org.springframework.security.AuthenticationException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
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
|
||||
* @version $Id$
|
||||
*/
|
||||
public class StatusCheckingUserDetailsService implements UserDetailsService {
|
||||
private UserDetailsService delegate;
|
||||
public class AccountStatusUserDetailsChecker implements UserDetailsChecker {
|
||||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
|
||||
public StatusCheckingUserDetailsService(UserDetailsService userDetailsService) {
|
||||
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.");
|
||||
|
||||
public void check(UserDetails user) {
|
||||
if (!user.isAccountNonLocked()) {
|
||||
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",
|
||||
"User credentials have expired"));
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -50,14 +50,6 @@ import org.springframework.dao.DataRetrievalFailureException;
|
|||
public class SiteminderAuthenticationProviderTests extends TestCase {
|
||||
//~ Methods ========================================================================================================
|
||||
|
||||
public static void main(String[] args) {
|
||||
junit.textui.TestRunner.run(SiteminderAuthenticationProviderTests.class);
|
||||
}
|
||||
|
||||
public final void setUp() throws Exception {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
public void testAuthenticateFailsIfAccountExpired() {
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");
|
||||
|
||||
|
|
|
@ -478,11 +478,11 @@ public class AbstractProcessingFilterTests extends TestCase {
|
|||
|
||||
// Setup our test object, to grant access
|
||||
MockAbstractProcessingFilter filter = new MockAbstractProcessingFilter(true);
|
||||
filter.setDefaultTargetUrl("http://monkeymachine.co.uk/");
|
||||
filter.setDefaultTargetUrl("https://monkeymachine.co.uk/");
|
||||
filter.setAlwaysUseDefaultTargetUrl(true);
|
||||
|
||||
executeFilterInContainerSimulator(config, filter, request, response, chain);
|
||||
assertEquals("http://monkeymachine.co.uk/", response.getRedirectedUrl());
|
||||
assertEquals("https://monkeymachine.co.uk/", response.getRedirectedUrl());
|
||||
assertNotNull(SecurityContextHolder.getContext().getAuthentication());
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -34,9 +34,6 @@ import org.springframework.util.Assert;
|
|||
* enabled/disabled status of the <code>UserDetails</code> because this is
|
||||
* authentication-related and should have been enforced by another provider server.
|
||||
* <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>,
|
||||
* so additional properties such as email addresses, telephone numbers etc can easily be stored.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue