From 7aaf70d5827c9937089b48724727d9bbbabf1b9c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 17:01:43 -0500 Subject: [PATCH] DaoAuthenticationProvider supports password upgrades Issue: gh-2778 --- .../dao/DaoAuthenticationProvider.java | 22 +++++ .../dao/DaoAuthenticationProviderTests.java | 80 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java index 07c34e0a93..0c4e86b574 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java @@ -20,12 +20,14 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; 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.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.util.Assert; /** @@ -62,6 +64,8 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication private UserDetailsService userDetailsService; + private UserDetailsPasswordService userDetailsPasswordService; + public DaoAuthenticationProvider() { setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); } @@ -120,6 +124,19 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication } } + @Override + protected Authentication createSuccessAuthentication(Object principal, + Authentication authentication, UserDetails user) { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(user.getPassword()); + if (upgradeEncoding) { + String presentedPassword = authentication.getCredentials().toString(); + String newPassword = this.passwordEncoder.encode(presentedPassword); + user = this.userDetailsPasswordService.updatePassword(user, newPassword); + } + return super.createSuccessAuthentication(principal, authentication, user); + } + private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); @@ -157,4 +174,9 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication protected UserDetailsService getUserDetailsService() { return userDetailsService; } + + public void setUserDetailsPasswordService( + UserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 094c79ebee..5f7c6e0a8d 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -17,12 +17,16 @@ package org.springframework.security.authentication.dao; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; 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.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.security.SecureRandom; @@ -43,6 +47,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio 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.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -53,6 +58,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; /** * Tests {@link DaoAuthenticationProvider}. @@ -399,6 +405,80 @@ public class DaoAuthenticationProviderTests { assertThat(castResult.getPrincipal()).isEqualTo("rod"); } + @Test + public void authenticateWhenSuccessAndPasswordManagerThenUpdates() { + String password = "password"; + String encodedPassword = "encoded"; + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", password); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(true); + when(encoder.encode(any())).thenReturn(encodedPassword); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + when(passwordManager.updatePassword(any(), any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verify(encoder).encode(password); + verify(passwordManager).updatePassword(eq(user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenBadCredentialsAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + assertThatThrownBy(() -> provider.authenticate(token)) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(passwordManager); + } + + @Test + public void authenticateWhenNotUpgradeAndPasswordManagerThenNoUpdate() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + "user", "password"); + + PasswordEncoder encoder = mock(PasswordEncoder.class); + UserDetailsService userDetailsService = mock(UserDetailsService.class); + UserDetailsPasswordService passwordManager = mock(UserDetailsPasswordService.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + provider.setUserDetailsService(userDetailsService); + provider.setUserDetailsPasswordService(passwordManager); + + UserDetails user = PasswordEncodedUser.user(); + when(encoder.matches(any(), any())).thenReturn(true); + when(encoder.upgradeEncoding(any())).thenReturn(false); + when(userDetailsService.loadUserByUsername(any())).thenReturn(user); + + Authentication result = provider.authenticate(token); + + verifyZeroInteractions(passwordManager); + } + @Test public void testDetectsNullBeingReturnedFromAuthenticationDao() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(