/* * Copyright 2004, 2005, 2006 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 org.springframework.security.authentication.dao; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import org.junit.Test; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.encoding.ShaPasswordEncoder; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache; import org.springframework.security.core.userdetails.cache.NullUserCache; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * Tests {@link DaoAuthenticationProvider}. * * @author Ben Alex * @author Rob Winch */ public class DaoAuthenticationProviderTests { private static final List<GrantedAuthority> ROLES_12 = AuthorityUtils.createAuthorityList( "ROLE_ONE", "ROLE_TWO"); // ~ Methods // ======================================================================================================== @Test public void testAuthenticateFailsForIncorrectPasswordCase() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "KOala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testReceivedBadCredentialsWhenCredentialsNotProvided() { // Test related to SEC-434 DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( "rod", null); try { provider.authenticate(authenticationToken); fail("Expected BadCredenialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticateFailsIfAccountExpired() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "peter", "opal"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService( new MockAuthenticationDaoUserPeterAccountExpired()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown AccountExpiredException"); } catch (AccountExpiredException expected) { } } @Test public void testAuthenticateFailsIfAccountLocked() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "peter", "opal"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserPeterAccountLocked()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown LockedException"); } catch (LockedException expected) { } } @Test public void testAuthenticateFailsIfCredentialsExpired() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "peter", "opal"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService( new MockAuthenticationDaoUserPeterCredentialsExpired()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown CredentialsExpiredException"); } catch (CredentialsExpiredException expected) { } // Check that wrong password causes BadCredentialsException, rather than // CredentialsExpiredException token = new UsernamePasswordAuthenticationToken("peter", "wrong_password"); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticateFailsIfUserDisabled() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "peter", "opal"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserPeter()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown DisabledException"); } catch (DisabledException expected) { } } @Test public void testAuthenticateFailsWhenAuthenticationDaoHasBackendFailure() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoSimulateBackendError()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown InternalAuthenticationServiceException"); } catch (InternalAuthenticationServiceException expected) { } } @Test public void testAuthenticateFailsWithEmptyUsername() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( null, "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticateFailsWithInvalidPassword() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "INVALID_PASSWORD"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionFalse() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "INVALID_USER", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); // we want // UsernameNotFoundExceptions provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown UsernameNotFoundException"); } catch (UsernameNotFoundException expected) { } } @Test public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionsWithDefaultOfTrue() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "INVALID_USER", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); assertThat(provider.isHideUserNotFoundExceptions()).isTrue(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "RoD", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); try { provider.authenticate(token); fail("Should have thrown BadCredentialsException"); } catch (BadCredentialsException expected) { } } @Test public void testAuthenticates() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); token.setDetails("192.168.0.1"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); Authentication result = provider.authenticate(token); if (!(result instanceof UsernamePasswordAuthenticationToken)) { fail("Should have returned instance of UsernamePasswordAuthenticationToken"); } UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result; assertThat(castResult.getPrincipal().getClass()).isEqualTo(User.class); assertThat(castResult.getCredentials()).isEqualTo("koala"); assertThat( AuthorityUtils.authorityListToSet(castResult.getAuthorities())).contains( "ROLE_ONE", "ROLE_TWO"); assertThat(castResult.getDetails()).isEqualTo("192.168.0.1"); } @Test public void testAuthenticatesASecondTime() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); 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"); } assertThat(result2.getCredentials()).isEqualTo(result.getCredentials()); } @Test public void testAuthenticatesWhenASaltIsUsed() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); SystemWideSaltSource salt = new SystemWideSaltSource(); salt.setSystemWideSalt("SYSTEM_SALT_VALUE"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrodWithSalt()); provider.setSaltSource(salt); provider.setUserCache(new MockUserCache()); Authentication result = provider.authenticate(token); if (!(result instanceof UsernamePasswordAuthenticationToken)) { fail("Should have returned instance of UsernamePasswordAuthenticationToken"); } assertThat(result.getPrincipal().getClass()).isEqualTo(User.class); // We expect original credentials user submitted to be returned assertThat(result.getCredentials()).isEqualTo("koala"); assertThat(AuthorityUtils.authorityListToSet(result.getAuthorities())).contains( "ROLE_ONE", "ROLE_TWO"); } @Test public void testAuthenticatesWithForcePrincipalAsString() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); provider.setUserCache(new MockUserCache()); provider.setForcePrincipalAsString(true); Authentication result = provider.authenticate(token); if (!(result instanceof UsernamePasswordAuthenticationToken)) { fail("Should have returned instance of UsernamePasswordAuthenticationToken"); } UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result; assertThat(castResult.getPrincipal().getClass()).isEqualTo(String.class); assertThat(castResult.getPrincipal()).isEqualTo("rod"); } @Test public void testDetectsNullBeingReturnedFromAuthenticationDao() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoReturnsNull()); try { provider.authenticate(token); fail("Should have thrown AuthenticationServiceException"); } catch (AuthenticationServiceException expected) { assertThat( "UserDetailsService returned null, which is an interface contract violation").isEqualTo( expected.getMessage()); } } @Test public void testGettersSetters() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(new ShaPasswordEncoder()); assertThat(provider.getPasswordEncoder().getClass()).isEqualTo( ShaPasswordEncoder.class); provider.setSaltSource(new SystemWideSaltSource()); assertThat(provider.getSaltSource().getClass()).isEqualTo( SystemWideSaltSource.class); provider.setUserCache(new EhCacheBasedUserCache()); assertThat(provider.getUserCache().getClass()).isEqualTo( EhCacheBasedUserCache.class); assertThat(provider.isForcePrincipalAsString()).isFalse(); provider.setForcePrincipalAsString(true); assertThat(provider.isForcePrincipalAsString()).isTrue(); } @Test public void testGoesBackToAuthenticationDaoToObtainLatestPasswordIfCachedPasswordSeemsIncorrect() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "rod", "koala"); MockAuthenticationDaoUserrod authenticationDao = new MockAuthenticationDaoUserrod(); MockUserCache cache = new MockUserCache(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(authenticationDao); provider.setUserCache(cache); // This will work, as password still "koala" provider.authenticate(token); // Check "rod = koala" ended up in the cache assertThat(cache.getUserFromCache("rod").getPassword()).isEqualTo("koala"); // Now change the password the AuthenticationDao will return authenticationDao.setPassword("easternLongNeckTurtle"); // Now try authentication again, with the new password token = new UsernamePasswordAuthenticationToken("rod", "easternLongNeckTurtle"); provider.authenticate(token); // To get this far, the new password was accepted // Check the cache was updated assertThat(cache.getUserFromCache("rod").getPassword()).isEqualTo( "easternLongNeckTurtle"); } @Test public void testStartupFailsIfNoAuthenticationDao() throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); try { provider.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { } } @Test public void testStartupFailsIfNoUserCacheSet() throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); assertThat(provider.getUserCache().getClass()).isEqualTo(NullUserCache.class); provider.setUserCache(null); try { provider.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { } } @Test public void testStartupSuccess() throws Exception { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); UserDetailsService userDetailsService = new MockAuthenticationDaoUserrod(); provider.setUserDetailsService(userDetailsService); provider.setUserCache(new MockUserCache()); assertThat(provider.getUserDetailsService()).isEqualTo(userDetailsService); provider.afterPropertiesSet(); } @Test public void testSupports() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); assertThat(provider.supports(UsernamePasswordAuthenticationToken.class)).isTrue(); assertThat(!provider.supports(TestingAuthenticationToken.class)).isTrue(); } // SEC-2056 @Test public void testUserNotFoundEncodesPassword() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "missing", "koala"); PasswordEncoder encoder = mock(PasswordEncoder.class); when(encoder.encode(anyString())).thenReturn("koala"); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setPasswordEncoder(encoder); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); try { provider.authenticate(token); fail("Expected Exception"); } catch (UsernameNotFoundException success) { } // ensure encoder invoked w/ non-null strings since PasswordEncoder impls may fail // if encoded password is null verify(encoder).matches(isA(String.class), isA(String.class)); } @Test public void testUserNotFoundBCryptPasswordEncoder() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "missing", "koala"); PasswordEncoder encoder = new BCryptPasswordEncoder(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setPasswordEncoder(encoder); MockAuthenticationDaoUserrod userDetailsService = new MockAuthenticationDaoUserrod(); userDetailsService.password = encoder.encode( (CharSequence) token.getCredentials()); provider.setUserDetailsService(userDetailsService); try { provider.authenticate(token); fail("Expected Exception"); } catch (UsernameNotFoundException success) { } } @Test public void testUserNotFoundDefaultEncoder() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "missing", null); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); try { provider.authenticate(token); fail("Expected Exception"); } catch (UsernameNotFoundException success) { } } /** * This is an explicit test for SEC-2056. It is intentionally ignored since this test * is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that * SEC-2056 is fixed. */ public void IGNOREtestSec2056() { UsernamePasswordAuthenticationToken foundUser = new UsernamePasswordAuthenticationToken( "rod", "koala"); UsernamePasswordAuthenticationToken notFoundUser = new UsernamePasswordAuthenticationToken( "notFound", "koala"); PasswordEncoder encoder = new BCryptPasswordEncoder(10, new SecureRandom()); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setPasswordEncoder(encoder); MockAuthenticationDaoUserrod userDetailsService = new MockAuthenticationDaoUserrod(); userDetailsService.password = encoder.encode( (CharSequence) foundUser.getCredentials()); provider.setUserDetailsService(userDetailsService); int sampleSize = 100; List<Long> userFoundTimes = new ArrayList<Long>(sampleSize); for (int i = 0; i < sampleSize; i++) { long start = System.currentTimeMillis(); provider.authenticate(foundUser); userFoundTimes.add(System.currentTimeMillis() - start); } List<Long> userNotFoundTimes = new ArrayList<Long>(sampleSize); for (int i = 0; i < sampleSize; i++) { long start = System.currentTimeMillis(); try { provider.authenticate(notFoundUser); fail("Expected Exception"); } catch (UsernameNotFoundException success) { } userNotFoundTimes.add(System.currentTimeMillis() - start); } double userFoundAvg = avg(userFoundTimes); double userNotFoundAvg = avg(userNotFoundTimes); assertThat(Math.abs(userNotFoundAvg - userFoundAvg) <= 3).withFailMessage( "User not found average " + userNotFoundAvg + " should be within 3ms of user found average " + userFoundAvg).isTrue(); } private double avg(List<Long> counts) { long sum = 0; for (Long time : counts) { sum += time; } return sum / counts.size(); } @Test public void testUserNotFoundNullCredentials() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "missing", null); PasswordEncoder encoder = mock(PasswordEncoder.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setPasswordEncoder(encoder); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); try { provider.authenticate(token); fail("Expected Exception"); } catch (UsernameNotFoundException success) { } verify(encoder, times(0)).matches(anyString(), anyString()); } // ~ Inner Classes // ================================================================================================== private class MockAuthenticationDaoReturnsNull implements UserDetailsService { public UserDetails loadUserByUsername(String username) { return null; } } private class MockAuthenticationDaoSimulateBackendError implements UserDetailsService { public UserDetails loadUserByUsername(String username) { throw new DataRetrievalFailureException( "This mock simulator is designed to fail"); } } private class MockAuthenticationDaoUserrod implements UserDetailsService { private String password = "koala"; public UserDetails loadUserByUsername(String username) { if ("rod".equals(username)) { return new User("rod", password, true, true, true, true, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } public void setPassword(String password) { this.password = password; } } private class MockAuthenticationDaoUserrodWithSalt implements UserDetailsService { public UserDetails loadUserByUsername(String username) { if ("rod".equals(username)) { return new User("rod", "koala{SYSTEM_SALT_VALUE}", true, true, true, true, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } } private class MockAuthenticationDaoUserPeter implements UserDetailsService { public UserDetails loadUserByUsername(String username) { if ("peter".equals(username)) { return new User("peter", "opal", false, true, true, true, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } } private class MockAuthenticationDaoUserPeterAccountExpired implements UserDetailsService { public UserDetails loadUserByUsername(String username) { if ("peter".equals(username)) { return new User("peter", "opal", true, false, true, true, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } } private class MockAuthenticationDaoUserPeterAccountLocked implements UserDetailsService { public UserDetails loadUserByUsername(String username) { if ("peter".equals(username)) { return new User("peter", "opal", true, true, true, false, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } } private class MockAuthenticationDaoUserPeterCredentialsExpired implements UserDetailsService { public UserDetails loadUserByUsername(String username) { if ("peter".equals(username)) { return new User("peter", "opal", true, true, false, true, ROLES_12); } else { throw new UsernameNotFoundException("Could not find: " + username); } } } }