19
19
import java .net .URI ;
20
20
import java .util .UUID ;
21
21
22
+ import jakarta .servlet .http .HttpServletRequest ;
23
+ import jakarta .servlet .http .HttpServletResponse ;
22
24
import org .junit .jupiter .api .Test ;
23
25
import org .junit .jupiter .api .extension .ExtendWith ;
24
26
28
30
import org .springframework .http .ResponseEntity ;
29
31
import org .springframework .mock .web .MockHttpSession ;
30
32
import 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 ;
32
35
import org .springframework .security .authentication .password .SimpleChangePasswordAdvice ;
33
36
import org .springframework .security .authentication .password .UserDetailsPasswordManager ;
34
37
import org .springframework .security .config .Customizer ;
37
40
import org .springframework .security .config .test .SpringTestContext ;
38
41
import org .springframework .security .config .test .SpringTestContextExtension ;
39
42
import org .springframework .security .core .annotation .AuthenticationPrincipal ;
40
- import org .springframework .security .core .userdetails .User ;
43
+ import org .springframework .security .core .userdetails .PasswordEncodedUser ;
41
44
import org .springframework .security .core .userdetails .UserDetails ;
42
45
import org .springframework .security .core .userdetails .UserDetailsService ;
43
46
import org .springframework .security .crypto .factory .PasswordEncoderFactories ;
44
47
import org .springframework .security .crypto .password .PasswordEncoder ;
45
48
import org .springframework .security .provisioning .InMemoryUserDetailsManager ;
46
49
import 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 ;
47
53
import org .springframework .test .web .servlet .MockMvc ;
48
54
import org .springframework .test .web .servlet .MvcResult ;
49
55
import org .springframework .web .bind .annotation .GetMapping ;
@@ -125,9 +131,8 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
125
131
this .mvc .perform (get ("/" ).with (user (admin ))).andExpect (status ().isOk ());
126
132
// change the password to a test value
127
133
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 ());
131
136
// admin "expires" their own password
132
137
this .mvc .perform (post ("/admin/passwords/expire/admin" ).with (csrf ()).with (user (admin )))
133
138
.andExpect (status ().isCreated ());
@@ -144,9 +149,8 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
144
149
.andExpect (redirectedUrl ("/change-password" ));
145
150
// reset the password to update
146
151
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 ());
150
154
// now we're good
151
155
this .mvc .perform (get ("/" ).session (session )).andExpect (status ().isOk ());
152
156
}
@@ -155,14 +159,14 @@ void whenAdminSetsExpiredAdviceThenUserLoginRedirectsToResetPassword() throws Ex
155
159
void whenCompromisedThenUserLoginAllowed () throws Exception {
156
160
this .spring .register (PasswordManagementConfig .class , AdminController .class , HomeController .class ).autowire ();
157
161
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" ))
159
163
.andExpect (status ().isFound ())
160
164
.andExpect (redirectedUrl ("/" ))
161
165
.andReturn ();
162
166
MockHttpSession session = (MockHttpSession ) result .getRequest ().getSession ();
163
167
this .mvc .perform (get ("/" ).session (session ))
164
168
.andExpect (status ().isOk ())
165
- .andExpect (content ().string (containsString ("COMPROMISED " )));
169
+ .andExpect (content ().string (containsString ("compromised " )));
166
170
}
167
171
168
172
@ Configuration
@@ -207,8 +211,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
207
211
// @formatter:off
208
212
http
209
213
.authorizeHttpRequests ((authz ) -> authz
210
- .requestMatchers ("/admin/**" ).hasRole ("ADMIN" )
211
- .anyRequest ().authenticated ()
214
+ .requestMatchers ("/admin/**" ).hasRole ("ADMIN" )
215
+ .anyRequest ().authenticated ()
212
216
)
213
217
.formLogin (Customizer .withDefaults ())
214
218
.passwordManagement (Customizer .withDefaults ());
@@ -219,8 +223,9 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
219
223
@ Bean
220
224
UserDetailsService users () {
221
225
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 ();
224
229
return new InMemoryUserDetailsManager (compromised , admin );
225
230
}
226
231
@@ -234,11 +239,9 @@ static class AdminController {
234
239
235
240
private final UserDetailsPasswordManager passwords ;
236
241
237
- private final PasswordEncoder encoder = PasswordEncoderFactories .createDelegatingPasswordEncoder ();
238
-
239
- AdminController (UserDetailsService users ) {
242
+ AdminController (UserDetailsService users , UserDetailsPasswordManager passwords ) {
240
243
this .users = users ;
241
- this .passwords = ( UserDetailsPasswordManager ) users ;
244
+ this .passwords = passwords ;
242
245
}
243
246
244
247
@ GetMapping ("/advice/{username}" )
@@ -258,29 +261,48 @@ ResponseEntity<ChangePasswordAdvice> expirePassword(@PathVariable("username") St
258
261
return ResponseEntity .notFound ().build ();
259
262
}
260
263
ChangePasswordAdvice advice = new SimpleChangePasswordAdvice (ChangePasswordAdvice .Action .MUST_CHANGE ,
261
- ChangePasswordReason .EXPIRED );
264
+ ChangePasswordReasons .EXPIRED );
262
265
this .passwords .savePasswordAdvice (user , advice );
263
266
URI uri = URI .create ("/admin/passwords/advice/" + username );
264
267
return ResponseEntity .created (uri ).body (advice );
265
268
}
266
269
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
-
274
270
}
275
271
276
272
@ RestController
277
273
static class HomeController {
278
274
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
+
279
289
@ GetMapping
280
290
ChangePasswordAdvice index (ChangePasswordAdvice advice ) {
281
291
return advice ;
282
292
}
283
293
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
+ }
284
306
}
285
307
286
308
}
0 commit comments