11/*
2- * Copyright 2002-2024 the original author or authors.
2+ * Copyright 2002-2025 the original author or authors.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
2121
2222import org .junit .jupiter .api .Test ;
2323import org .junit .jupiter .api .extension .ExtendWith ;
24+ import org .mockito .ArgumentMatchers ;
25+ import org .mockito .Mockito ;
2426import reactor .core .publisher .Mono ;
2527
2628import org .springframework .beans .factory .annotation .Autowired ;
4042import org .springframework .security .test .web .reactive .server .SecurityMockServerConfigurers ;
4143import org .springframework .security .web .server .SecurityWebFilterChain ;
4244import org .springframework .security .web .server .authentication .RedirectServerAuthenticationSuccessHandler ;
45+ import org .springframework .security .web .server .authentication .ott .DefaultServerGenerateOneTimeTokenRequestResolver ;
46+ import org .springframework .security .web .server .authentication .ott .ServerGenerateOneTimeTokenRequestResolver ;
4347import org .springframework .security .web .server .authentication .ott .ServerOneTimeTokenGenerationSuccessHandler ;
4448import org .springframework .security .web .server .authentication .ott .ServerRedirectOneTimeTokenGenerationSuccessHandler ;
4549import org .springframework .test .web .reactive .server .WebTestClient ;
4953
5054import static org .assertj .core .api .Assertions .assertThat ;
5155import static org .assertj .core .api .Assertions .assertThatException ;
56+ import static org .mockito .Mockito .times ;
57+ import static org .mockito .Mockito .verify ;
5258
5359/**
5460 * Tests for {@link ServerHttpSecurity.OneTimeTokenLoginSpec}
@@ -107,7 +113,7 @@ void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() {
107113 .expectHeader ().valueEquals ("Location" , "/login/ott" );
108114 // @formatter:on
109115
110- String token = TestServerOneTimeTokenGenerationSuccessHandler . lastToken .getTokenValue ();
116+ String token = getLastToken () .getTokenValue ();
111117
112118 // @formatter:off
113119 this .client .mutateWith (SecurityMockServerConfigurers .csrf ())
@@ -143,7 +149,7 @@ void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() {
143149 .expectHeader ().valueEquals ("Location" , "/redirected" );
144150 // @formatter:on
145151
146- String token = TestServerOneTimeTokenGenerationSuccessHandler . lastToken .getTokenValue ();
152+ String token = getLastToken () .getTokenValue ();
147153
148154 // @formatter:off
149155 this .client .mutateWith (SecurityMockServerConfigurers .csrf ())
@@ -179,7 +185,7 @@ void oneTimeTokenWhenCorrectTokenUsedTwiceThenSecondTimeFails() {
179185 .expectHeader ().valueEquals ("Location" , "/login/ott" );
180186 // @formatter:on
181187
182- String token = TestServerOneTimeTokenGenerationSuccessHandler . lastToken .getTokenValue ();
188+ String token = getLastToken () .getTokenValue ();
183189
184190 // @formatter:off
185191 this .client .mutateWith (SecurityMockServerConfigurers .csrf ())
@@ -268,6 +274,12 @@ void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() {
268274 assertThat (response .contains (GENERATE_OTT_PART )).isTrue ();
269275 }
270276
277+ private OneTimeToken getLastToken () {
278+ OneTimeToken lastToken = this .spring .getContext ()
279+ .getBean (TestServerOneTimeTokenGenerationSuccessHandler .class ).lastToken ;
280+ return lastToken ;
281+ }
282+
271283 @ Test
272284 void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException () {
273285 assertThatException ()
@@ -280,27 +292,58 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
280292 """ );
281293 }
282294
295+ @ Test
296+ void oneTimeTokenWhenCustomRequestResolverSetThenCustomResolverUse () {
297+ this .spring .register (OneTimeTokenConfigWithCustomRequestResolver .class ).autowire ();
298+
299+ // @formatter:off
300+ this .client .mutateWith (SecurityMockServerConfigurers .csrf ())
301+ .post ()
302+ .uri ((uriBuilder ) -> uriBuilder
303+ .path ("/ott/generate" )
304+ .build ()
305+ )
306+ .contentType (MediaType .APPLICATION_FORM_URLENCODED )
307+ .body (BodyInserters .fromFormData ("username" , "user" ))
308+ .exchange ()
309+ .expectStatus ()
310+ .is3xxRedirection ()
311+ .expectHeader ().valueEquals ("Location" , "/login/ott" );
312+ // @formatter:on
313+
314+ ServerGenerateOneTimeTokenRequestResolver resolver = this .spring .getContext ()
315+ .getBean (ServerGenerateOneTimeTokenRequestResolver .class );
316+
317+ verify (resolver , times (1 )).resolve (ArgumentMatchers .any (ServerWebExchange .class ));
318+ }
319+
283320 @ Configuration (proxyBeanMethods = false )
284321 @ EnableWebFlux
285322 @ EnableWebFluxSecurity
286323 @ Import (UserDetailsServiceConfig .class )
287324 static class OneTimeTokenDefaultConfig {
288325
289326 @ Bean
290- SecurityWebFilterChain securityWebFilterChain (ServerHttpSecurity http ) {
327+ SecurityWebFilterChain securityWebFilterChain (ServerHttpSecurity http ,
328+ ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler ) {
291329 // @formatter:off
292330 http
293331 .authorizeExchange ((authorize ) -> authorize
294332 .anyExchange ()
295333 .authenticated ()
296334 )
297335 .oneTimeTokenLogin ((ott ) -> ott
298- .tokenGenerationSuccessHandler (new TestServerOneTimeTokenGenerationSuccessHandler () )
336+ .tokenGenerationSuccessHandler (ottSuccessHandler )
299337 );
300338 // @formatter:on
301339 return http .build ();
302340 }
303341
342+ @ Bean
343+ TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler () {
344+ return new TestServerOneTimeTokenGenerationSuccessHandler ();
345+ }
346+
304347 }
305348
306349 @ Configuration (proxyBeanMethods = false )
@@ -310,7 +353,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
310353 static class OneTimeTokenDifferentUrlsConfig {
311354
312355 @ Bean
313- SecurityWebFilterChain securityFilterChain (ServerHttpSecurity http ) {
356+ SecurityWebFilterChain securityFilterChain (ServerHttpSecurity http ,
357+ ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler ) {
314358 // @formatter:off
315359 http
316360 .authorizeExchange ((authorize ) -> authorize
@@ -319,14 +363,19 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
319363 )
320364 .oneTimeTokenLogin ((ott ) -> ott
321365 .tokenGeneratingUrl ("/generateurl" )
322- .tokenGenerationSuccessHandler (new TestServerOneTimeTokenGenerationSuccessHandler ( "/redirected" ) )
366+ .tokenGenerationSuccessHandler (ottSuccessHandler )
323367 .loginProcessingUrl ("/loginprocessingurl" )
324368 .authenticationSuccessHandler (new RedirectServerAuthenticationSuccessHandler ("/authenticated" ))
325369 );
326370 // @formatter:on
327371 return http .build ();
328372 }
329373
374+ @ Bean
375+ TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler () {
376+ return new TestServerOneTimeTokenGenerationSuccessHandler ("/redirected" );
377+ }
378+
330379 }
331380
332381 @ Configuration (proxyBeanMethods = false )
@@ -336,7 +385,8 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
336385 static class OneTimeTokenFormLoginConfig {
337386
338387 @ Bean
339- SecurityWebFilterChain securityFilterChain (ServerHttpSecurity http ) {
388+ SecurityWebFilterChain securityFilterChain (ServerHttpSecurity http ,
389+ ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler ) {
340390 // @formatter:off
341391 http
342392 .authorizeExchange ((authorize ) -> authorize
@@ -345,12 +395,17 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
345395 )
346396 .formLogin (Customizer .withDefaults ())
347397 .oneTimeTokenLogin ((ott ) -> ott
348- .tokenGenerationSuccessHandler (new TestServerOneTimeTokenGenerationSuccessHandler () )
398+ .tokenGenerationSuccessHandler (ottSuccessHandler )
349399 );
350400 // @formatter:on
351401 return http .build ();
352402 }
353403
404+ @ Bean
405+ TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler () {
406+ return new TestServerOneTimeTokenGenerationSuccessHandler ();
407+ }
408+
354409 }
355410
356411 @ Configuration (proxyBeanMethods = false )
@@ -385,10 +440,44 @@ ReactiveUserDetailsService userDetailsService() {
385440
386441 }
387442
443+ @ Configuration (proxyBeanMethods = false )
444+ @ EnableWebFlux
445+ @ EnableWebFluxSecurity
446+ @ Import (UserDetailsServiceConfig .class )
447+ static class OneTimeTokenConfigWithCustomRequestResolver {
448+
449+ @ Bean
450+ SecurityWebFilterChain securityWebFilterChain (ServerHttpSecurity http ,
451+ ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler ) {
452+ // @formatter:off
453+ http
454+ .authorizeExchange ((authorize ) -> authorize
455+ .anyExchange ()
456+ .authenticated ()
457+ )
458+ .oneTimeTokenLogin ((ott ) -> ott
459+ .tokenGenerationSuccessHandler (ottSuccessHandler )
460+ );
461+ // @formatter:on
462+ return http .build ();
463+ }
464+
465+ @ Bean
466+ ServerGenerateOneTimeTokenRequestResolver resolver () {
467+ return Mockito .spy (new DefaultServerGenerateOneTimeTokenRequestResolver ());
468+ }
469+
470+ @ Bean
471+ TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler () {
472+ return new TestServerOneTimeTokenGenerationSuccessHandler ();
473+ }
474+
475+ }
476+
388477 private static class TestServerOneTimeTokenGenerationSuccessHandler
389478 implements ServerOneTimeTokenGenerationSuccessHandler {
390479
391- private static OneTimeToken lastToken ;
480+ private OneTimeToken lastToken ;
392481
393482 private final ServerOneTimeTokenGenerationSuccessHandler delegate ;
394483
@@ -402,7 +491,7 @@ private static class TestServerOneTimeTokenGenerationSuccessHandler
402491
403492 @ Override
404493 public Mono <Void > handle (ServerWebExchange exchange , OneTimeToken oneTimeToken ) {
405- lastToken = oneTimeToken ;
494+ this . lastToken = oneTimeToken ;
406495 return this .delegate .handle (exchange , oneTimeToken );
407496 }
408497
0 commit comments