diff --git a/core/src/main/java/org/springframework/security/providers/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/providers/dao/DaoAuthenticationProvider.java index 1b51ccb15e..d17c2d3e91 100644 --- a/core/src/main/java/org/springframework/security/providers/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/providers/dao/DaoAuthenticationProvider.java @@ -24,6 +24,7 @@ import org.springframework.security.providers.encoding.PasswordEncoder; import org.springframework.security.providers.encoding.PlaintextPasswordEncoder; import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetailsService; +import org.springframework.security.userdetails.UsernameNotFoundException; import org.springframework.dao.DataAccessException; import org.springframework.util.Assert; @@ -35,10 +36,24 @@ import org.springframework.util.Assert; * @version $Id$ */ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { + //~ Static fields/initializers ===================================================================================== + + /** + * The plaintext password used to perform {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is + * not found to avoid SEC-2056. + */ + private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; //~ Instance fields ================================================================================================ - private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder(); + private PasswordEncoder passwordEncoder; + + /** + * The password used to perform {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is + * not found to avoid SEC-2056. This is necessary, because some {@link PasswordEncoder} implementations will short circuit if the + * password is not in a valid format. + */ + private String userNotFoundEncodedPassword; private SaltSource saltSource; @@ -46,6 +61,10 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private boolean includeDetailsObject = true; + public DaoAuthenticationProvider() { + setPasswordEncoder(new PlaintextPasswordEncoder()); + } + //~ Methods ======================================================================================================== protected void additionalAuthenticationChecks(UserDetails userDetails, @@ -85,6 +104,13 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication catch (DataAccessException repositoryProblem) { throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem); } + catch (UsernameNotFoundException notFound) { + if(authentication.getCredentials() != null) { + String presentedPassword = authentication.getCredentials().toString(); + passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); + } + throw notFound; + } if (loadedUser == null) { throw new AuthenticationServiceException( @@ -100,6 +126,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication * @param passwordEncoder The passwordEncoder to use */ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + this.userNotFoundEncodedPassword = passwordEncoder.encodePassword(USER_NOT_FOUND_PASSWORD, null); this.passwordEncoder = passwordEncoder; } @@ -143,6 +170,6 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication */ public void setIncludeDetailsObject(boolean includeDetailsObject) { this.includeDetailsObject = includeDetailsObject; - } + } } diff --git a/core/src/test/java/org/springframework/security/providers/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/providers/dao/DaoAuthenticationProviderTests.java index 5053aa246e..541b33ad28 100644 --- a/core/src/test/java/org/springframework/security/providers/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/providers/dao/DaoAuthenticationProviderTests.java @@ -15,6 +15,13 @@ package org.springframework.security.providers.dao; +import static org.mockito.Matchers.anyObject; +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 junit.framework.TestCase; import org.springframework.security.AccountExpiredException; @@ -32,6 +39,7 @@ import org.springframework.security.providers.UsernamePasswordAuthenticationToke import org.springframework.security.providers.dao.cache.EhCacheBasedUserCache; import org.springframework.security.providers.dao.cache.NullUserCache; import org.springframework.security.providers.dao.salt.SystemWideSaltSource; +import org.springframework.security.providers.encoding.PasswordEncoder; import org.springframework.security.providers.encoding.ShaPasswordEncoder; import org.springframework.security.userdetails.User; @@ -436,6 +444,40 @@ public class DaoAuthenticationProviderTests extends TestCase { assertTrue(!provider.supports(TestingAuthenticationToken.class)); } + + // SEC-2056 + public void testUserNotFoundEncodesPassword() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", "koala"); + PasswordEncoder encoder = mock(PasswordEncoder.class); + when(encoder.encodePassword(anyString(), anyObject())).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).isPasswordValid(isA(String.class), isA(String.class), anyObject()); + } + + 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)).isPasswordValid(anyString(), anyString(), anyObject()); + } + //~ Inner Classes ================================================================================================== private class MockAuthenticationDaoReturnsNull implements UserDetailsService {