فهرست منبع

Add support for password-validating DAOs, such as LDAP. Contributed by Karel Miarka.

Ben Alex 21 سال پیش
والد
کامیت
1a92434914

+ 1 - 0
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

+ 3 - 2
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).
 

+ 76 - 0
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.
+ * 
+ * <p>
+ * Used with the {@link PasswordDaoAuthenticationProvider}.
+ * </p>
+ * 
+ * <p>
+ * The interface requires only one read-only method, which simplifies support
+ * of new data access strategies.
+ * </p>
+ *
+ * @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 <code>UserDetails</code> object that comes back may
+     * have a username that is of a different case than what was actually
+     * requested.
+     * 
+     * <p>
+     * The implementation is responsible for password validation. It must throw
+     * <code>BadCredentialsException</code> (or subclass of that exception if
+     * desired) if the provided password is invalid.
+     * </p>
+     * 
+     * <p>
+     * The implementation is responsible for filling the username and password
+     * parameters into the implementation of <code>UserDetails</code>.
+     * </p>
+     *
+     * @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
+     *         <code>GrantedAuthority</code>s
+     */
+    public UserDetails loadUserByUsernameAndPassword(String username,
+        String password) throws DataAccessException, BadCredentialsException;
+}

+ 276 - 0
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}.
+ * 
+ * <p>
+ * This <code>AuthenticationProvider</code> is capable of validating {@link
+ * UsernamePasswordAuthenticationToken} requests containing the correct
+ * username, password and when the user is not disabled.
+ * </p>
+ * 
+ * <p>
+ * Unlike {@link DaoAuthenticationProvider}, the responsibility for password
+ * validation is delegated to <code>PasswordAuthenticationDao</code>.
+ * </p>
+ * 
+ * <p>
+ * Upon successful validation, a
+ * <code>UsernamePasswordAuthenticationToken</code> will be created and
+ * returned to the caller. The token will include as its principal either a
+ * <code>String</code> representation of the username, or the {@link
+ * UserDetails} that was returned from the authentication repository. Using
+ * <code>String</code> is appropriate if a container adapter is being used, as
+ * it expects <code>String</code> representations of the username. Using
+ * <code>UserDetails</code> 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 <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 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.
+ * </p>
+ * 
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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.
+     * 
+     * <P>
+     * Protected so subclasses can override. This might be required if multiple
+     * credentials need to be placed into a custom <code>Authentication</code>
+     * object, such as a password as well as a ZIP code.
+     * </p>
+     * 
+     * <P>
+     * Subclasses will usually store the original credentials the user supplied
+     * (not salted or encoded passwords) in the returned
+     * <code>Authentication</code> object.
+     * </p>
+     *
+     * @param principal that should be the principal in the returned object
+     *        (defined by the {@link #isForcePrincipalAsString()} method)
+     * @param authentication that was presented to the
+     *        <code>PasswordDaoAuthenticationProvider</code> for validation
+     * @param user that was loaded by the
+     *        <code>PasswordAuthenticationDao</code>
+     *
+     * @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);
+        }
+    }
+}

+ 43 - 0
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.
+ * 
+ * <P>
+ * <code>AuthenticationFailureUsernameOrPasswordEvent.getUser()</code> returns
+ * an instance of <code>User</code>, where the username is filled by the
+ * <code>String</code> provided at login attempt. The other properties are set
+ * to non-<code>null</code> values without any meaning.
+ * </p>
+ *
+ * @author Karel Miarka
+ */
+public class AuthenticationFailureUsernameOrPasswordEvent
+    extends AuthenticationEvent {
+    //~ Constructors ===========================================================
+
+    public AuthenticationFailureUsernameOrPasswordEvent(
+        Authentication authentication, UserDetails user) {
+        super(authentication, user);
+    }
+}

+ 11 - 0
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;
 

+ 309 - 0
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) {}
+    }
+}