mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-16 00:52:12 +00:00
SEC-2056: DaoAuthenticationProvider performs isPasswordValid when user not found
Previously authenticating a user could take significantly longer than determining that a user does not exist. This was due to the fact that only users that were found would use the password encoder and comparing a password can take a significant amount of time. The difference in the time required could allow a side channel attack that reveals if a user exists. The code has been updated to do comparison against a dummy password even when the the user was not found.
This commit is contained in:
parent
a4f13a9ae0
commit
f5fc94e1be
@ -24,6 +24,7 @@ import org.springframework.security.providers.encoding.PasswordEncoder;
|
|||||||
import org.springframework.security.providers.encoding.PlaintextPasswordEncoder;
|
import org.springframework.security.providers.encoding.PlaintextPasswordEncoder;
|
||||||
import org.springframework.security.userdetails.UserDetails;
|
import org.springframework.security.userdetails.UserDetails;
|
||||||
import org.springframework.security.userdetails.UserDetailsService;
|
import org.springframework.security.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
@ -35,10 +36,24 @@ import org.springframework.util.Assert;
|
|||||||
* @version $Id$
|
* @version $Id$
|
||||||
*/
|
*/
|
||||||
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
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 ================================================================================================
|
//~ 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;
|
private SaltSource saltSource;
|
||||||
|
|
||||||
@ -46,6 +61,10 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
|
|||||||
|
|
||||||
private boolean includeDetailsObject = true;
|
private boolean includeDetailsObject = true;
|
||||||
|
|
||||||
|
public DaoAuthenticationProvider() {
|
||||||
|
setPasswordEncoder(new PlaintextPasswordEncoder());
|
||||||
|
}
|
||||||
|
|
||||||
//~ Methods ========================================================================================================
|
//~ Methods ========================================================================================================
|
||||||
|
|
||||||
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
||||||
@ -85,6 +104,13 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
|
|||||||
catch (DataAccessException repositoryProblem) {
|
catch (DataAccessException repositoryProblem) {
|
||||||
throw new AuthenticationServiceException(repositoryProblem.getMessage(), 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) {
|
if (loadedUser == null) {
|
||||||
throw new AuthenticationServiceException(
|
throw new AuthenticationServiceException(
|
||||||
@ -100,6 +126,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
|
|||||||
* @param passwordEncoder The passwordEncoder to use
|
* @param passwordEncoder The passwordEncoder to use
|
||||||
*/
|
*/
|
||||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||||
|
this.userNotFoundEncodedPassword = passwordEncoder.encodePassword(USER_NOT_FOUND_PASSWORD, null);
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,13 @@
|
|||||||
|
|
||||||
package org.springframework.security.providers.dao;
|
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 junit.framework.TestCase;
|
||||||
|
|
||||||
import org.springframework.security.AccountExpiredException;
|
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.EhCacheBasedUserCache;
|
||||||
import org.springframework.security.providers.dao.cache.NullUserCache;
|
import org.springframework.security.providers.dao.cache.NullUserCache;
|
||||||
import org.springframework.security.providers.dao.salt.SystemWideSaltSource;
|
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.providers.encoding.ShaPasswordEncoder;
|
||||||
|
|
||||||
import org.springframework.security.userdetails.User;
|
import org.springframework.security.userdetails.User;
|
||||||
@ -436,6 +444,40 @@ public class DaoAuthenticationProviderTests extends TestCase {
|
|||||||
assertTrue(!provider.supports(TestingAuthenticationToken.class));
|
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 ==================================================================================================
|
//~ Inner Classes ==================================================================================================
|
||||||
|
|
||||||
private class MockAuthenticationDaoReturnsNull implements UserDetailsService {
|
private class MockAuthenticationDaoReturnsNull implements UserDetailsService {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user