diff --git a/changelog.txt b/changelog.txt index 793cf542d3..5408a4618e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,7 @@ Changes in version 0.x (2004-xx-xx) * Added additional DaoAuthenticationProvider event when user not found * Added Authentication.getDetails() to DaoAuthenticationProvider response * Added DaoAuthenticationProvider.hideUserNotFoundExceptions (default=true) +* Added PasswordAuthenticationProvider for password-validating DAOs (eg LDAP) * Extracted removeUserFromCache(String) to UserCache interface * Improved ConfigAttributeEditor so it trims preceding and trailing spaces * Fixed EH-CACHE-based caching implementation behaviour when cache exists diff --git a/contributors.txt b/contributors.txt index b26ca86a12..7e9332ad91 100644 --- a/contributors.txt +++ b/contributors.txt @@ -30,8 +30,9 @@ contributions to the Acegi Security System for Spring project: * Ray Krueger is a current member of the development team. -* Karel Miarka contributed a fix for EH-CACHE NPEs and additional event - handling for DaoAuthenticationProvider. +* Karel Miarka contributed a fix for EH-CACHE NPEs, additional event + handling for DaoAuthenticationProvider, and the + PasswordAuthenticationProvider-related classes * Anyone else I've forgotten (please let me know so I can correct this). diff --git a/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java b/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java new file mode 100644 index 0000000000..fc56936873 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java @@ -0,0 +1,76 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed 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 net.sf.acegisecurity.providers.dao; + +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.UserDetails; + +import org.springframework.dao.DataAccessException; + + +/** + * Defines an interface for DAO implementations capable of locating and also + * validating a password. + * + *

+ * Used with the {@link PasswordDaoAuthenticationProvider}. + *

+ * + *

+ * The interface requires only one read-only method, which simplifies support + * of new data access strategies. + *

+ * + * @author Karel Miarka + */ +public interface PasswordAuthenticationDao { + //~ Methods ================================================================ + + /** + * Locates the user based on the username and password. In the actual + * implementation, the search may possibly be case sensitive, or case + * insensitive depending on how the implementaion instance is configured. + * In this case, the UserDetails object that comes back may + * have a username that is of a different case than what was actually + * requested. + * + *

+ * The implementation is responsible for password validation. It must throw + * BadCredentialsException (or subclass of that exception if + * desired) if the provided password is invalid. + *

+ * + *

+ * The implementation is responsible for filling the username and password + * parameters into the implementation of UserDetails. + *

+ * + * @param username the username presented to the {@link + * PasswordDaoAuthenticationProvider} + * @param password the password presented to the {@link + * PasswordDaoAuthenticationProvider} + * + * @return a fully populated user record + * + * @throws DataAccessException if user could not be found for a + * repository-specific reason + * @throws BadCredentialsException if the user could not be found, invalid + * password provided or the user has no + * GrantedAuthoritys + */ + public UserDetails loadUserByUsernameAndPassword(String username, + String password) throws DataAccessException, BadCredentialsException; +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java new file mode 100644 index 0000000000..a1757b7389 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java @@ -0,0 +1,276 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed 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 net.sf.acegisecurity.providers.dao; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.AuthenticationServiceException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.DisabledException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.AuthenticationProvider; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.cache.NullUserCache; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureUsernameOrPasswordEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import org.springframework.dao.DataAccessException; + + +/** + * An {@link AuthenticationProvider} implementation that retrieves user details + * from a {@link PasswordAuthenticationDao}. + * + *

+ * This AuthenticationProvider is capable of validating {@link + * UsernamePasswordAuthenticationToken} requests containing the correct + * username, password and when the user is not disabled. + *

+ * + *

+ * Unlike {@link DaoAuthenticationProvider}, the responsibility for password + * validation is delegated to PasswordAuthenticationDao. + *

+ * + *

+ * Upon successful validation, a + * UsernamePasswordAuthenticationToken will be created and + * returned to the caller. The token will include as its principal either a + * String representation of the username, or the {@link + * UserDetails} that was returned from the authentication repository. Using + * String is appropriate if a container adapter is being used, as + * it expects String representations of the username. Using + * UserDetails is appropriate if you require access to additional + * properties of the authenticated user, such as email addresses, + * human-friendly names etc. As container adapters are not recommended to be + * used, and UserDetails implementations provide additional + * flexibility, by default a UserDetails is returned. To override + * this default, set the {@link #setForcePrincipalAsString} to + * true. + *

+ * + *

+ * Caching is handled via the UserDetails object being placed in + * the {@link UserCache}. This ensures that subsequent requests with the same + * username and password can be validated without needing to query the {@link + * PasswordAuthenticationDao}. It should be noted that if a user appears to + * present an incorrect password, the {@link PasswordAuthenticationDao} will + * be queried to confirm the most up-to-date password was used for comparison. + *

+ * + *

+ * If an application context is detected (which is automatically the case when + * the bean is started within a Spring container), application events will be + * published to the context. See {@link + * net.sf.acegisecurity.providers.dao.event.AuthenticationEvent} for further + * information. + *

+ * + * @author Karel Miarka + */ +public class PasswordDaoAuthenticationProvider implements AuthenticationProvider, + InitializingBean, ApplicationContextAware { + //~ Instance fields ======================================================== + + private ApplicationContext context; + private PasswordAuthenticationDao authenticationDao; + private UserCache userCache = new NullUserCache(); + private boolean forcePrincipalAsString = false; + + //~ Methods ================================================================ + + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + + public ApplicationContext getContext() { + return context; + } + + public void setForcePrincipalAsString(boolean forcePrincipalAsString) { + this.forcePrincipalAsString = forcePrincipalAsString; + } + + public boolean isForcePrincipalAsString() { + return forcePrincipalAsString; + } + + public void setPasswordAuthenticationDao( + PasswordAuthenticationDao authenticationDao) { + this.authenticationDao = authenticationDao; + } + + public PasswordAuthenticationDao getPasswordAuthenticationDao() { + return authenticationDao; + } + + public void setUserCache(UserCache userCache) { + this.userCache = userCache; + } + + public UserCache getUserCache() { + return userCache; + } + + public void afterPropertiesSet() throws Exception { + if (this.authenticationDao == null) { + throw new IllegalArgumentException( + "A Password authentication DAO must be set"); + } + + if (this.userCache == null) { + throw new IllegalArgumentException("A user cache must be set"); + } + } + + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + // Determine username + String username = authentication.getPrincipal().toString(); + + if (authentication.getPrincipal() instanceof UserDetails) { + username = ((UserDetails) authentication.getPrincipal()) + .getUsername(); + } + + String password = authentication.getCredentials().toString(); + + boolean cacheWasUsed = true; + UserDetails user = this.userCache.getUserFromCache(username); + + // Check if the provided password is the same as the password in cache + if ((user != null) && !password.equals(user.getPassword())) { + user = null; + this.userCache.removeUserFromCache(username); + } + + if (user == null) { + cacheWasUsed = false; + + try { + user = getUserFromBackend(username, password); + } catch (BadCredentialsException ex) { + if (this.context != null) { + if ((username == null) || "".equals(username)) { + username = "NONE_PROVIDED"; + } + + context.publishEvent(new AuthenticationFailureUsernameOrPasswordEvent( + authentication, + new User(username, "*****", false, + new GrantedAuthority[0]))); + } + + throw ex; + } + } + + if (!user.isEnabled()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureDisabledEvent( + authentication, user)); + } + + throw new DisabledException("User is disabled"); + } + + if (!cacheWasUsed) { + // Put into cache + this.userCache.putUserInCache(user); + + // As this appears to be an initial login, publish the event + if (this.context != null) { + context.publishEvent(new AuthenticationSuccessEvent( + authentication, user)); + } + } + + Object principalToReturn = user; + + if (forcePrincipalAsString) { + principalToReturn = user.getUsername(); + } + + return createSuccessAuthentication(principalToReturn, authentication, + user); + } + + public boolean supports(Class authentication) { + if (UsernamePasswordAuthenticationToken.class.isAssignableFrom( + authentication)) { + return true; + } else { + return false; + } + } + + /** + * Creates a successful {@link Authentication} object. + * + *

+ * Protected so subclasses can override. This might be required if multiple + * credentials need to be placed into a custom Authentication + * object, such as a password as well as a ZIP code. + *

+ * + *

+ * Subclasses will usually store the original credentials the user supplied + * (not salted or encoded passwords) in the returned + * Authentication object. + *

+ * + * @param principal that should be the principal in the returned object + * (defined by the {@link #isForcePrincipalAsString()} method) + * @param authentication that was presented to the + * PasswordDaoAuthenticationProvider for validation + * @param user that was loaded by the + * PasswordAuthenticationDao + * + * @return the successful authentication token + */ + protected Authentication createSuccessAuthentication(Object principal, + Authentication authentication, UserDetails user) { + // Ensure we return the original credentials the user supplied, + // so subsequent attempts are successful even with encoded passwords. + // Also ensure we return the original getDetails(), so that future + // authentication events after cache expiry contain the details + UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, + authentication.getCredentials(), user.getAuthorities()); + result.setDetails((authentication.getDetails() != null) + ? authentication.getDetails().toString() : null); + + return result; + } + + private UserDetails getUserFromBackend(String username, String password) { + try { + return this.authenticationDao.loadUserByUsernameAndPassword(username, + password); + } catch (DataAccessException repositoryProblem) { + throw new AuthenticationServiceException(repositoryProblem + .getMessage(), repositoryProblem); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java new file mode 100644 index 0000000000..d0661af000 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java @@ -0,0 +1,43 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed 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 net.sf.acegisecurity.providers.dao.event; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.UserDetails; + + +/** + * Application event which indicates authentication failure due to invalid + * username or password. + * + *

+ * AuthenticationFailureUsernameOrPasswordEvent.getUser() returns + * an instance of User, where the username is filled by the + * String provided at login attempt. The other properties are set + * to non-null values without any meaning. + *

+ * + * @author Karel Miarka + */ +public class AuthenticationFailureUsernameOrPasswordEvent + extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationFailureUsernameOrPasswordEvent( + Authentication authentication, UserDetails user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java b/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java index a1bfa3622e..b6db1a080b 100644 --- a/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java @@ -74,6 +74,17 @@ public class LoggerListener implements ApplicationListener { } } + if (event instanceof AuthenticationFailureUsernameOrPasswordEvent) { + AuthenticationFailureUsernameOrPasswordEvent authEvent = (AuthenticationFailureUsernameOrPasswordEvent) event; + + if (logger.isWarnEnabled()) { + logger.warn( + "Authentication failed due to invalid username or password: " + + authEvent.getUser().getUsername() + "; details: " + + authEvent.getAuthentication().getDetails()); + } + } + if (event instanceof AuthenticationSuccessEvent) { AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event; diff --git a/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java new file mode 100644 index 0000000000..e6ddb631ea --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java @@ -0,0 +1,309 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * Licensed 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 net.sf.acegisecurity.providers.dao; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationServiceException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.DisabledException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.providers.TestingAuthenticationToken; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache; +import net.sf.acegisecurity.providers.dao.cache.NullUserCache; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataRetrievalFailureException; + +import java.util.HashMap; +import java.util.Map; + + +/** + * Tests {@link PasswordDaoAuthenticationProvider}. + * + * @author Karel Miarka + */ +public class PasswordDaoAuthenticationProviderTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(PasswordDaoAuthenticationProviderTests.class); + } + + public void testAuthenticateFailsForIncorrectPasswordCase() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", + "KOala"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsIfUserDisabled() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", + "opal"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserPeter()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown DisabledException"); + } catch (DisabledException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsWhenAuthenticationDaoHasBackendFailure() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", + "koala"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoSimulateBackendError()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown AuthenticationServiceException"); + } catch (AuthenticationServiceException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsWithInvalidPassword() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", + "INVALID_PASSWORD"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsWithInvalidUsername() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", + "koala"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("MaRiSSA", + "koala"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertTrue(true); + } + } + + public void testAuthenticates() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", + "koala"); + token.setDetails("192.168.0.1"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + Authentication result = provider.authenticate(token); + + if (!(result instanceof UsernamePasswordAuthenticationToken)) { + fail( + "Should have returned instance of UsernamePasswordAuthenticationToken"); + } + + UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result; + assertEquals(User.class, castResult.getPrincipal().getClass()); + assertEquals("koala", castResult.getCredentials()); + assertEquals("ROLE_ONE", castResult.getAuthorities()[0].getAuthority()); + assertEquals("ROLE_TWO", castResult.getAuthorities()[1].getAuthority()); + assertEquals("192.168.0.1", castResult.getDetails()); + } + + public void testAuthenticatesASecondTime() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", + "koala"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + provider.setUserCache(new MockUserCache()); + + Authentication result = provider.authenticate(token); + + if (!(result instanceof UsernamePasswordAuthenticationToken)) { + fail( + "Should have returned instance of UsernamePasswordAuthenticationToken"); + } + + // Now try to authenticate with the previous result (with its UserDetails) + Authentication result2 = provider.authenticate(result); + + if (!(result2 instanceof UsernamePasswordAuthenticationToken)) { + fail( + "Should have returned instance of UsernamePasswordAuthenticationToken"); + } + + assertEquals(result.getCredentials(), result2.getCredentials()); + } + + public void testGettersSetters() { + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setUserCache(new EhCacheBasedUserCache()); + assertEquals(EhCacheBasedUserCache.class, + provider.getUserCache().getClass()); + + assertFalse(provider.isForcePrincipalAsString()); + provider.setForcePrincipalAsString(true); + assertTrue(provider.isForcePrincipalAsString()); + } + + public void testStartupFailsIfNoAuthenticationDao() + throws Exception { + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + + try { + provider.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testStartupFailsIfNoUserCacheSet() throws Exception { + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + assertEquals(NullUserCache.class, provider.getUserCache().getClass()); + provider.setUserCache(null); + + try { + provider.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testStartupSuccess() throws Exception { + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + PasswordAuthenticationDao dao = new MockAuthenticationDaoUserMarissa(); + provider.setPasswordAuthenticationDao(dao); + provider.setUserCache(new MockUserCache()); + assertEquals(dao, provider.getPasswordAuthenticationDao()); + provider.afterPropertiesSet(); + assertTrue(true); + } + + public void testSupports() { + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + assertTrue(provider.supports(UsernamePasswordAuthenticationToken.class)); + assertTrue(!provider.supports(TestingAuthenticationToken.class)); + } + + //~ Inner Classes ========================================================== + + private class MockAuthenticationDaoSimulateBackendError + implements PasswordAuthenticationDao { + public UserDetails loadUserByUsernameAndPassword(String username, + String password) + throws BadCredentialsException, DataAccessException { + throw new DataRetrievalFailureException( + "This mock simulator is designed to fail"); + } + } + + private class MockAuthenticationDaoUserMarissa + implements PasswordAuthenticationDao { + public UserDetails loadUserByUsernameAndPassword(String username, + String password) + throws BadCredentialsException, DataAccessException { + if ("marissa".equals(username) && "koala".equals(password)) { + return new User("marissa", "koala", true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new BadCredentialsException("Invalid credentials"); + } + } + } + + private class MockAuthenticationDaoUserPeter + implements PasswordAuthenticationDao { + public UserDetails loadUserByUsernameAndPassword(String username, + String password) + throws BadCredentialsException, DataAccessException { + if ("peter".equals(username) && "opal".equals(password)) { + return new User("peter", "opal", false, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new BadCredentialsException("Invalid credentials"); + } + } + } + + private class MockUserCache implements UserCache { + private Map cache = new HashMap(); + + public UserDetails getUserFromCache(String username) { + return (User) cache.get(username); + } + + public void putUserInCache(UserDetails user) { + cache.put(user.getUsername(), user); + } + + public void removeUserFromCache(String username) {} + } +}