From 72a267a311fe08d49ff9a2e3f05f3fdd65c7c6fb Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 14 Jul 2018 23:28:53 -0500 Subject: [PATCH] UserDetailsRepositoryReactiveAuthenticationManager uses ReactiveUserDetailsPasswordService Issue: gh-2778 --- ...positoryReactiveAuthenticationManager.java | 27 ++++++++- ...oryReactiveAuthenticationManagerTests.java | 59 ++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index c60ea15f5c..7e67677eb4 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -18,7 +18,9 @@ package org.springframework.security.authentication; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; @@ -38,6 +40,8 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + private Scheduler scheduler = Schedulers.parallel(); public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { @@ -48,11 +52,21 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React @Override public Mono authenticate(Authentication authentication) { final String username = authentication.getName(); + final String presentedPassword = (String) authentication.getCredentials(); return this.userDetailsService.findByUsername(username) .publishOn(this.scheduler) - .filter( u -> this.passwordEncoder.matches((String) authentication.getCredentials(), u.getPassword())) + .filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword())) .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) - .map( u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); + .flatMap(u -> { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(u.getPassword()); + if (upgradeEncoding) { + String newPassword = this.passwordEncoder.encode(presentedPassword); + return this.userDetailsPasswordService.updatePassword(u, newPassword); + } + return Mono.just(u); + }) + .map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); } /** @@ -80,4 +94,13 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React Assert.notNull(scheduler, "scheduler cannot be null"); this.scheduler = scheduler; } + + /** + * Sets the service to use for upgrading passwords on successful authentication. + * @param userDetailsPasswordService the service to use + */ + public void setUserDetailsPasswordService( + ReactiveUserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } } diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java index 238c075fc1..4de034dcb5 100644 --- a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -32,7 +33,9 @@ import reactor.core.scheduler.Schedulers; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; /** @@ -47,6 +50,9 @@ public class UserDetailsRepositoryReactiveAuthenticationManagerTests { @Mock private PasswordEncoder encoder; + @Mock + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + @Mock private Scheduler scheduler; @@ -79,10 +85,61 @@ public class UserDetailsRepositoryReactiveAuthenticationManagerTests { this.manager.setScheduler(this.scheduler); this.manager.setPasswordEncoder(this.encoder); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( - this.user, this.user.getPassword()); + this.user, this.user.getPassword()); Authentication result = this.manager.authenticate(token).block(); verify(this.scheduler).schedule(any()); } + + @Test + public void authenticateWhenPasswordServiceThenUpdated() { + String encodedPassword = "encoded"; + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(true); + when(this.encoder.encode(any())).thenReturn(encodedPassword); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verify(this.encoder).encode(this.user.getPassword()); + verify(this.userDetailsPasswordService).updatePassword(eq(this.user), eq(encodedPassword)); + } + + @Test + public void authenticateWhenPasswordServiceAndBadCredentialsThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(false); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + assertThatThrownBy(() -> this.manager.authenticate(token).block()) + .isInstanceOf(BadCredentialsException.class); + + verifyZeroInteractions(this.userDetailsPasswordService); + } + + @Test + public void authenticateWhenPasswordServiceAndUpgradeFalseThenNotUpdated() { + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(this.user)); + when(this.encoder.matches(any(), any())).thenReturn(true); + when(this.encoder.upgradeEncoding(any())).thenReturn(false); + when(this.userDetailsPasswordService.updatePassword(any(), any())).thenReturn(Mono.just(this.user)); + this.manager.setPasswordEncoder(this.encoder); + this.manager.setUserDetailsPasswordService(this.userDetailsPasswordService); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + this.user, this.user.getPassword()); + + Authentication result = this.manager.authenticate(token).block(); + + verifyZeroInteractions(this.userDetailsPasswordService); + } }