Skip to content

Commit 93d7dbd

Browse files
Add Support ServerGenerateOneTimeTokenRequestResolver
Closes gh-16488 Signed-off-by: Max Batischev <mblancer@mail.ru>
1 parent 7030a62 commit 93d7dbd

File tree

9 files changed

+408
-10
lines changed

9 files changed

+408
-10
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -29,6 +29,7 @@
2929
import java.util.Iterator;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Objects;
3233
import java.util.UUID;
3334
import java.util.function.Consumer;
3435
import java.util.function.Function;
@@ -53,6 +54,7 @@
5354
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
5455
import org.springframework.security.authentication.ReactiveAuthenticationManager;
5556
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
57+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
5658
import org.springframework.security.authentication.ott.OneTimeToken;
5759
import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
5860
import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager;
@@ -156,7 +158,9 @@
156158
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
157159
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
158160
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
161+
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
159162
import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter;
163+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
160164
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter;
161165
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
162166
import org.springframework.security.web.server.authorization.AuthorizationContext;
@@ -5940,6 +5944,8 @@ public final class OneTimeTokenLoginSpec {
59405944

59415945
private ServerSecurityContextRepository securityContextRepository;
59425946

5947+
private ServerGenerateOneTimeTokenRequestResolver requestResolver;
5948+
59435949
private String loginProcessingUrl = "/login/ott";
59445950

59455951
private String defaultSubmitPageUrl = "/login/ott";
@@ -5985,6 +5991,7 @@ private void configureOttGenerateFilter(ServerHttpSecurity http) {
59855991
getTokenGenerationSuccessHandler());
59865992
generateFilter
59875993
.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
5994+
generateFilter.setGenerateRequestResolver(getRequestResolver());
59885995
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
59895996
}
59905997

@@ -6112,6 +6119,32 @@ public OneTimeTokenLoginSpec authenticationConverter(ServerAuthenticationConvert
61126119
return this;
61136120
}
61146121

6122+
/**
6123+
* Use this {@link ServerGenerateOneTimeTokenRequestResolver} when resolving
6124+
* {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}. By default,
6125+
* the {@link DefaultServerGenerateOneTimeTokenRequestResolver} is used.
6126+
* @param requestResolver the
6127+
* {@link DefaultServerGenerateOneTimeTokenRequestResolver} to use
6128+
* @since 6.5
6129+
*/
6130+
public OneTimeTokenLoginSpec generateRequestResolver(
6131+
ServerGenerateOneTimeTokenRequestResolver requestResolver) {
6132+
Assert.notNull(requestResolver, "generateRequestResolver cannot be null");
6133+
this.requestResolver = requestResolver;
6134+
return this;
6135+
}
6136+
6137+
private ServerGenerateOneTimeTokenRequestResolver getRequestResolver() {
6138+
if (this.requestResolver != null) {
6139+
return this.requestResolver;
6140+
}
6141+
ServerGenerateOneTimeTokenRequestResolver bean = getBeanOrNull(
6142+
ServerGenerateOneTimeTokenRequestResolver.class);
6143+
this.requestResolver = Objects.requireNonNullElseGet(bean,
6144+
DefaultServerGenerateOneTimeTokenRequestResolver::new);
6145+
return this.requestResolver;
6146+
}
6147+
61156148
/**
61166149
* Specifies the URL to process the login request, defaults to {@code /login/ott}.
61176150
* Only POST requests are processed, for that reason make sure that you pass a

config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -18,6 +18,7 @@ package org.springframework.security.config.web.server
1818

1919
import org.springframework.security.authentication.ReactiveAuthenticationManager
2020
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
21+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
2122
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
2223
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
2324
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
@@ -34,6 +35,7 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo
3435
* @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
3536
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
3637
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used
38+
* @property generateRequestResolver the [ServerGenerateOneTimeTokenRequestResolver] to be used
3739
* @property defaultSubmitPageUrl sets the URL that the default submit page will be generated
3840
* @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown
3941
* @property loginProcessingUrl the URL to process the login request
@@ -50,6 +52,7 @@ class ServerOneTimeTokenLoginDsl {
5052
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
5153
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
5254
var securityContextRepository: ServerSecurityContextRepository? = null
55+
var generateRequestResolver: ServerGenerateOneTimeTokenRequestResolver? = null
5356
var defaultSubmitPageUrl: String? = null
5457
var loginProcessingUrl: String? = null
5558
var tokenGeneratingUrl: String? = null
@@ -71,6 +74,7 @@ class ServerOneTimeTokenLoginDsl {
7174
)
7275
}
7376
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
77+
generateRequestResolver?.also { oneTimeTokenLogin.generateRequestResolver(generateRequestResolver) }
7478
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
7579
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
7680
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }

config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java

+67-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.security.config.web.server;
1818

19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.time.ZoneOffset;
1922
import java.util.Collections;
2023
import java.util.Map;
2124

@@ -29,6 +32,7 @@
2932
import org.springframework.context.annotation.Configuration;
3033
import org.springframework.context.annotation.Import;
3134
import org.springframework.http.MediaType;
35+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
3236
import org.springframework.security.authentication.ott.OneTimeToken;
3337
import org.springframework.security.config.Customizer;
3438
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
@@ -40,6 +44,8 @@
4044
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers;
4145
import org.springframework.security.web.server.SecurityWebFilterChain;
4246
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
47+
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
48+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
4349
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
4450
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler;
4551
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -280,6 +286,36 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
280286
""");
281287
}
282288

289+
@Test
290+
void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception {
291+
this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire();
292+
293+
// @formatter:off
294+
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
295+
.post()
296+
.uri((uriBuilder) -> uriBuilder
297+
.path("/ott/generate")
298+
.build()
299+
)
300+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
301+
.body(BodyInserters.fromFormData("username", "user"))
302+
.exchange()
303+
.expectStatus()
304+
.is3xxRedirection()
305+
.expectHeader().valueEquals("Location", "/login/ott");
306+
// @formatter:on
307+
308+
OneTimeToken token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken;
309+
310+
assertThat(getCurrentMinutes(token.getExpiresAt())).isEqualTo(10);
311+
}
312+
313+
private int getCurrentMinutes(Instant expiresAt) {
314+
int expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).getMinute();
315+
int currentMinutes = Instant.now().atZone(ZoneOffset.UTC).getMinute();
316+
return expiresMinutes - currentMinutes;
317+
}
318+
283319
@Configuration(proxyBeanMethods = false)
284320
@EnableWebFlux
285321
@EnableWebFluxSecurity
@@ -385,6 +421,36 @@ ReactiveUserDetailsService userDetailsService() {
385421

386422
}
387423

424+
@Configuration(proxyBeanMethods = false)
425+
@EnableWebFlux
426+
@EnableWebFluxSecurity
427+
@Import(UserDetailsServiceConfig.class)
428+
static class OneTimeTokenConfigWithCustomTokenExpirationTime {
429+
430+
@Bean
431+
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
432+
// @formatter:off
433+
http
434+
.authorizeExchange((authorize) -> authorize
435+
.anyExchange()
436+
.authenticated()
437+
)
438+
.oneTimeTokenLogin((ott) -> ott
439+
.tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler())
440+
);
441+
// @formatter:on
442+
return http.build();
443+
}
444+
445+
@Bean
446+
ServerGenerateOneTimeTokenRequestResolver resolver() {
447+
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
448+
return (exchange) -> resolver.resolve(exchange)
449+
.map((request) -> new GenerateOneTimeTokenRequest(request.getUsername(), Duration.ofSeconds(600)));
450+
}
451+
452+
}
453+
388454
private static class TestServerOneTimeTokenGenerationSuccessHandler
389455
implements ServerOneTimeTokenGenerationSuccessHandler {
390456

config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt

+78-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.config.web.server
1818

19+
import org.assertj.core.api.Assertions
1920
import org.junit.jupiter.api.Test
2021
import org.junit.jupiter.api.extension.ExtendWith
2122
import reactor.core.publisher.Mono
@@ -26,6 +27,7 @@ import org.springframework.context.annotation.Configuration
2627
import org.springframework.context.annotation.Import
2728
import org.springframework.context.ApplicationContext
2829
import org.springframework.http.MediaType
30+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
2931
import org.springframework.security.authentication.ott.OneTimeToken
3032
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
3133
import org.springframework.security.config.test.SpringTestContext
@@ -34,6 +36,8 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
3436
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
3537
import org.springframework.security.core.userdetails.User
3638
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
39+
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver
40+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
3741
import org.springframework.security.web.server.SecurityWebFilterChain
3842
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
3943
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
@@ -43,6 +47,9 @@ import org.springframework.web.reactive.config.EnableWebFlux
4347
import org.springframework.web.reactive.function.BodyInserters
4448
import org.springframework.web.server.ServerWebExchange
4549
import org.springframework.web.util.UriBuilder
50+
import java.time.Duration
51+
import java.time.Instant
52+
import java.time.ZoneOffset
4653

4754
/**
4855
* Tests for [ServerOneTimeTokenLoginDsl]
@@ -146,6 +153,48 @@ class ServerOneTimeTokenLoginDslTests {
146153
// @formatter:on
147154
}
148155

156+
@Test
157+
fun `oneTimeToken when custom token expiration time set then authenticate`() {
158+
spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime::class.java).autowire()
159+
160+
// @formatter:off
161+
client.mutateWith(SecurityMockServerConfigurers.csrf())
162+
.post()
163+
.uri{ uriBuilder: UriBuilder -> uriBuilder
164+
.path("/ott/generate")
165+
.build()
166+
}
167+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
168+
.body(BodyInserters.fromFormData("username", "user"))
169+
.exchange()
170+
.expectStatus()
171+
.is3xxRedirection()
172+
.expectHeader().valueEquals("Location", "/login/ott")
173+
174+
client.mutateWith(SecurityMockServerConfigurers.csrf())
175+
.post()
176+
.uri{ uriBuilder:UriBuilder -> uriBuilder
177+
.path("/ott/generate")
178+
.build()
179+
}
180+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
181+
.body(BodyInserters.fromFormData("username", "user"))
182+
.exchange()
183+
.expectStatus()
184+
.is3xxRedirection()
185+
.expectHeader().valueEquals("Location", "/login/ott")
186+
187+
val token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken
188+
189+
Assertions.assertThat(getCurrentMinutes(token!!.expiresAt)).isEqualTo(10)
190+
}
191+
192+
private fun getCurrentMinutes(expiresAt:Instant): Int {
193+
val expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).minute
194+
val currentMinutes = Instant.now().atZone(ZoneOffset.UTC).minute
195+
return expiresMinutes - currentMinutes
196+
}
197+
149198
@Configuration
150199
@EnableWebFlux
151200
@EnableWebFluxSecurity
@@ -199,6 +248,34 @@ class ServerOneTimeTokenLoginDslTests {
199248
MapReactiveUserDetailsService(User("user", "password", listOf()))
200249
}
201250

251+
@Configuration(proxyBeanMethods = false)
252+
@EnableWebFlux
253+
@EnableWebFluxSecurity
254+
@Import(OneTimeTokenLoginSpecTests.UserDetailsServiceConfig::class)
255+
open class OneTimeTokenConfigWithCustomTokenExpirationTime {
256+
@Bean
257+
open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
258+
// @formatter:off
259+
return http {
260+
authorizeExchange {
261+
authorize(anyExchange, authenticated)
262+
}
263+
oneTimeTokenLogin {
264+
tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler()
265+
}
266+
}
267+
}
268+
269+
@Bean
270+
open fun resolver(): ServerGenerateOneTimeTokenRequestResolver {
271+
val resolver = DefaultServerGenerateOneTimeTokenRequestResolver()
272+
return ServerGenerateOneTimeTokenRequestResolver { exchange ->
273+
resolver.resolve(exchange)
274+
.map { request -> GenerateOneTimeTokenRequest(request.username, Duration.ofSeconds(600)) }
275+
}
276+
}
277+
}
278+
202279
private class TestServerOneTimeTokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler {
203280
private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null
204281

docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc

+32
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,35 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender):
546546
547547
----
548548
======
549+
550+
[[customize-generate-token-request]]
551+
== Customize GenerateOneTimeTokenRequest Instance
552+
There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default.
553+
554+
You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so:
555+
[tabs]
556+
======
557+
Java::
558+
+
559+
[source,java,role="primary"]
560+
----
561+
@Bean
562+
ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
563+
DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
564+
resolver.setExpiresIn(Duration.ofSeconds(600));
565+
return resolver;
566+
}
567+
----
568+
569+
Kotlin::
570+
+
571+
[source,kotlin,role="secondary"]
572+
----
573+
@Bean
574+
fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
575+
return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
576+
this.setExpiresIn(Duration.ofMinutes(10))
577+
}
578+
}
579+
----
580+
======

0 commit comments

Comments
 (0)