1919import java .net .URI ;
2020import java .util .UUID ;
2121
22+ import jakarta .servlet .http .HttpServletRequest ;
23+ import jakarta .servlet .http .HttpServletResponse ;
2224import org .junit .jupiter .api .Test ;
2325import org .junit .jupiter .api .extension .ExtendWith ;
2426
2830import org .springframework .http .ResponseEntity ;
2931import org .springframework .mock .web .MockHttpSession ;
3032import org .springframework .security .authentication .password .ChangePasswordAdvice ;
31- import org .springframework .security .authentication .password .ChangePasswordReason ;
33+ import org .springframework .security .authentication .password .ChangePasswordAdvisor ;
34+ import org .springframework .security .authentication .password .ChangePasswordReasons ;
3235import org .springframework .security .authentication .password .SimpleChangePasswordAdvice ;
3336import org .springframework .security .authentication .password .UserDetailsPasswordManager ;
3437import org .springframework .security .config .Customizer ;
3740import org .springframework .security .config .test .SpringTestContext ;
3841import org .springframework .security .config .test .SpringTestContextExtension ;
3942import org .springframework .security .core .annotation .AuthenticationPrincipal ;
40- import org .springframework .security .core .userdetails .User ;
43+ import org .springframework .security .core .userdetails .PasswordEncodedUser ;
4144import org .springframework .security .core .userdetails .UserDetails ;
4245import org .springframework .security .core .userdetails .UserDetailsService ;
4346import org .springframework .security .crypto .factory .PasswordEncoderFactories ;
4447import org .springframework .security .crypto .password .PasswordEncoder ;
4548import org .springframework .security .provisioning .InMemoryUserDetailsManager ;
4649import org .springframework .security .web .SecurityFilterChain ;
50+ import org .springframework .security .web .authentication .password .ChangeCompromisedPasswordAdvisor ;
51+ import org .springframework .security .web .authentication .password .ChangePasswordAdviceRepository ;
52+ import org .springframework .security .web .authentication .password .HttpSessionChangePasswordAdviceRepository ;
4753import org .springframework .test .web .servlet .MockMvc ;
4854import org .springframework .test .web .servlet .MvcResult ;
4955import org .springframework .web .bind .annotation .GetMapping ;
@@ -125,9 +131,8 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
125131 this .mvc .perform (get ("/" ).with (user (admin ))).andExpect (status ().isOk ());
126132 // change the password to a test value
127133 String random = UUID .randomUUID ().toString ();
128- this .mvc .perform (post ("/change-password" ).with (csrf ()).with (user (admin )).param ("newPassword" , random ))
129- .andExpect (status ().isFound ())
130- .andExpect (redirectedUrl ("/" ));
134+ this .mvc .perform (post ("/change-password" ).with (csrf ()).with (user (admin )).param ("password" , random ))
135+ .andExpect (status ().isOk ());
131136 // admin "expires" their own password
132137 this .mvc .perform (post ("/admin/passwords/expire/admin" ).with (csrf ()).with (user (admin )))
133138 .andExpect (status ().isCreated ());
@@ -144,9 +149,8 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
144149 .andExpect (redirectedUrl ("/change-password" ));
145150 // reset the password to update
146151 random = UUID .randomUUID ().toString ();
147- this .mvc .perform (post ("/change-password" ).with (csrf ()).session (session ).param ("newPassword" , random ))
148- .andExpect (status ().isFound ())
149- .andExpect (redirectedUrl ("/" ));
152+ this .mvc .perform (post ("/change-password" ).with (csrf ()).session (session ).param ("password" , random ))
153+ .andExpect (status ().isOk ());
150154 // now we're good
151155 this .mvc .perform (get ("/" ).session (session )).andExpect (status ().isOk ());
152156 }
@@ -155,14 +159,14 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
155159 void whenCompromisedThenUserLoginAllowed () throws Exception {
156160 this .spring .register (PasswordManagementConfig .class , AdminController .class , HomeController .class ).autowire ();
157161 MvcResult result = this .mvc
158- .perform (post ("/login" ).with (csrf ()).param ("username" , "compromised " ).param ("password" , "password" ))
162+ .perform (post ("/login" ).with (csrf ()).param ("username" , "user " ).param ("password" , "password" ))
159163 .andExpect (status ().isFound ())
160164 .andExpect (redirectedUrl ("/" ))
161165 .andReturn ();
162166 MockHttpSession session = (MockHttpSession ) result .getRequest ().getSession ();
163167 this .mvc .perform (get ("/" ).session (session ))
164168 .andExpect (status ().isOk ())
165- .andExpect (content ().string (containsString ("COMPROMISED " )));
169+ .andExpect (content ().string (containsString ("compromised " )));
166170 }
167171
168172 @ Configuration
@@ -207,8 +211,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
207211 // @formatter:off
208212 http
209213 .authorizeHttpRequests ((authz ) -> authz
210- .requestMatchers ("/admin/**" ).hasRole ("ADMIN" )
211- .anyRequest ().authenticated ()
214+ .requestMatchers ("/admin/**" ).hasRole ("ADMIN" )
215+ .anyRequest ().authenticated ()
212216 )
213217 .formLogin (Customizer .withDefaults ())
214218 .passwordManagement (Customizer .withDefaults ());
@@ -219,8 +223,9 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
219223 @ Bean
220224 UserDetailsService users () {
221225 String adminPassword = UUID .randomUUID ().toString ();
222- UserDetails compromised = User .withUsername ("compromised" ).password ("{noop}password" ).roles ("USER" ).build ();
223- UserDetails admin = User .withUsername ("admin" ).password ("{noop}" + adminPassword ).roles ("ADMIN" ).build ();
226+ UserDetails compromised = PasswordEncodedUser .user ();
227+ UserDetails admin = PasswordEncodedUser .withUserDetails (PasswordEncodedUser .admin ())
228+ .password (adminPassword ).build ();
224229 return new InMemoryUserDetailsManager (compromised , admin );
225230 }
226231
@@ -234,11 +239,9 @@ static class AdminController {
234239
235240 private final UserDetailsPasswordManager passwords ;
236241
237- private final PasswordEncoder encoder = PasswordEncoderFactories .createDelegatingPasswordEncoder ();
238-
239- AdminController (UserDetailsService users ) {
242+ AdminController (UserDetailsService users , UserDetailsPasswordManager passwords ) {
240243 this .users = users ;
241- this .passwords = ( UserDetailsPasswordManager ) users ;
244+ this .passwords = passwords ;
242245 }
243246
244247 @ GetMapping ("/advice/{username}" )
@@ -258,29 +261,48 @@ ResponseEntity<ChangePasswordAdvice> expirePassword(@PathVariable("username") St
258261 return ResponseEntity .notFound ().build ();
259262 }
260263 ChangePasswordAdvice advice = new SimpleChangePasswordAdvice (ChangePasswordAdvice .Action .MUST_CHANGE ,
261- ChangePasswordReason .EXPIRED );
264+ ChangePasswordReasons .EXPIRED );
262265 this .passwords .savePasswordAdvice (user , advice );
263266 URI uri = URI .create ("/admin/passwords/advice/" + username );
264267 return ResponseEntity .created (uri ).body (advice );
265268 }
266269
267- @ PostMapping ("/change" )
268- ResponseEntity <?> changePassword (@ AuthenticationPrincipal UserDetails user ,
269- @ RequestParam ("password" ) String password ) {
270- this .passwords .updatePassword (user , this .encoder .encode (password ));
271- return ResponseEntity .ok ().build ();
272- }
273-
274270 }
275271
276272 @ RestController
277273 static class HomeController {
278274
275+ private final UserDetailsPasswordManager passwords ;
276+
277+ private final ChangePasswordAdvisor changePasswordAdvisor =
278+ new ChangeCompromisedPasswordAdvisor ();
279+
280+ private final ChangePasswordAdviceRepository changePasswordAdviceRepository =
281+ new HttpSessionChangePasswordAdviceRepository ();
282+
283+ private final PasswordEncoder encoder = PasswordEncoderFactories .createDelegatingPasswordEncoder ();
284+
285+ HomeController (UserDetailsPasswordManager passwords ) {
286+ this .passwords = passwords ;
287+ }
288+
279289 @ GetMapping
280290 ChangePasswordAdvice index (ChangePasswordAdvice advice ) {
281291 return advice ;
282292 }
283293
294+ @ PostMapping ("/change-password" )
295+ ResponseEntity <?> changePassword (@ AuthenticationPrincipal UserDetails user ,
296+ @ RequestParam ("password" ) String password , HttpServletRequest request , HttpServletResponse response ) {
297+ ChangePasswordAdvice advice = this .changePasswordAdvisor .advise (user , password );
298+ if (advice .getAction () != ChangePasswordAdvice .Action .ABSTAIN ) {
299+ return ResponseEntity .badRequest ().body (advice );
300+ }
301+ this .passwords .updatePassword (user , this .encoder .encode (password ));
302+ this .passwords .removePasswordAdvice (user );
303+ this .changePasswordAdviceRepository .removePasswordAdvice (request , response );
304+ return ResponseEntity .ok ().build ();
305+ }
284306 }
285307
286308}
0 commit comments