Przeglądaj źródła

Add Support ServerGenerateOneTimeTokenRequestResolver

Closes gh-16488

Signed-off-by: Max Batischev <mblancer@mail.ru>
Max Batischev 6 miesięcy temu
rodzic
commit
be81377235

+ 34 - 1
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -53,6 +54,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
 import org.springframework.security.authentication.ott.OneTimeToken;
 import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
 import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager;
@@ -156,7 +158,9 @@ import org.springframework.security.web.server.authentication.logout.LogoutWebFi
 import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
 import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
 import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
 import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter;
+import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
 import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter;
 import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
 import org.springframework.security.web.server.authorization.AuthorizationContext;
@@ -5940,6 +5944,8 @@ public class ServerHttpSecurity {
 
 		private ServerSecurityContextRepository securityContextRepository;
 
+		private ServerGenerateOneTimeTokenRequestResolver requestResolver;
+
 		private String loginProcessingUrl = "/login/ott";
 
 		private String defaultSubmitPageUrl = "/login/ott";
@@ -5985,6 +5991,7 @@ public class ServerHttpSecurity {
 					getTokenGenerationSuccessHandler());
 			generateFilter
 				.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
+			generateFilter.setGenerateRequestResolver(getRequestResolver());
 			http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
 		}
 
@@ -6112,6 +6119,32 @@ public class ServerHttpSecurity {
 			return this;
 		}
 
+		/**
+		 * Use this {@link ServerGenerateOneTimeTokenRequestResolver} when resolving
+		 * {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}. By default,
+		 * the {@link DefaultServerGenerateOneTimeTokenRequestResolver} is used.
+		 * @param requestResolver the
+		 * {@link DefaultServerGenerateOneTimeTokenRequestResolver} to use
+		 * @since 6.5
+		 */
+		public OneTimeTokenLoginSpec generateRequestResolver(
+				ServerGenerateOneTimeTokenRequestResolver requestResolver) {
+			Assert.notNull(requestResolver, "generateRequestResolver cannot be null");
+			this.requestResolver = requestResolver;
+			return this;
+		}
+
+		private ServerGenerateOneTimeTokenRequestResolver getRequestResolver() {
+			if (this.requestResolver != null) {
+				return this.requestResolver;
+			}
+			ServerGenerateOneTimeTokenRequestResolver bean = getBeanOrNull(
+					ServerGenerateOneTimeTokenRequestResolver.class);
+			this.requestResolver = Objects.requireNonNullElseGet(bean,
+					DefaultServerGenerateOneTimeTokenRequestResolver::new);
+			return this.requestResolver;
+		}
+
 		/**
 		 * Specifies the URL to process the login request, defaults to {@code /login/ott}.
 		 * Only POST requests are processed, for that reason make sure that you pass a

+ 5 - 1
config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.config.web.server
 
 import org.springframework.security.authentication.ReactiveAuthenticationManager
 import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
+import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
 import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
 import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
 import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
@@ -34,6 +35,7 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo
  * @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
  * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
  * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used
+ * @property generateRequestResolver the [ServerGenerateOneTimeTokenRequestResolver] to be used
  * @property defaultSubmitPageUrl sets the URL that the default submit page will be generated
  * @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown
  * @property loginProcessingUrl the URL to process the login request
@@ -50,6 +52,7 @@ class ServerOneTimeTokenLoginDsl {
     var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
     var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
     var securityContextRepository: ServerSecurityContextRepository? = null
+    var generateRequestResolver: ServerGenerateOneTimeTokenRequestResolver? = null
     var defaultSubmitPageUrl: String? = null
     var loginProcessingUrl: String? = null
     var tokenGeneratingUrl: String? = null
@@ -71,6 +74,7 @@ class ServerOneTimeTokenLoginDsl {
                 )
             }
             securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
+            generateRequestResolver?.also { oneTimeTokenLogin.generateRequestResolver(generateRequestResolver) }
             defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
             showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
             loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }

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

@@ -271,10 +271,10 @@ public class OneTimeTokenLoginSpecTests {
 	@Test
 	void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() {
 		assertThatException()
-			.isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire())
-			.havingRootCause()
-			.isInstanceOf(IllegalStateException.class)
-			.withMessage("""
+				.isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire())
+				.havingRootCause()
+				.isInstanceOf(IllegalStateException.class)
+				.withMessage("""
 					A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
 					Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
 					""");

+ 78 - 1
config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.web.server
 
+import org.assertj.core.api.Assertions
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
 import reactor.core.publisher.Mono
@@ -26,6 +27,7 @@ import org.springframework.context.annotation.Configuration
 import org.springframework.context.annotation.Import
 import org.springframework.context.ApplicationContext
 import org.springframework.http.MediaType
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest
 import org.springframework.security.authentication.ott.OneTimeToken
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
 import org.springframework.security.config.test.SpringTestContext
@@ -34,6 +36,8 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
 import org.springframework.security.core.userdetails.ReactiveUserDetailsService
 import org.springframework.security.core.userdetails.User
 import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
+import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver
+import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
 import org.springframework.security.web.server.SecurityWebFilterChain
 import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
 import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
@@ -43,6 +47,9 @@ import org.springframework.web.reactive.config.EnableWebFlux
 import org.springframework.web.reactive.function.BodyInserters
 import org.springframework.web.server.ServerWebExchange
 import org.springframework.web.util.UriBuilder
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneOffset
 
 /**
  * Tests for [ServerOneTimeTokenLoginDsl]
@@ -146,6 +153,48 @@ class ServerOneTimeTokenLoginDslTests {
         // @formatter:on
     }
 
+    @Test
+    fun `oneTimeToken when custom token expiration time set then authenticate`() {
+        spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime::class.java).autowire()
+
+        // @formatter:off
+        client.mutateWith(SecurityMockServerConfigurers.csrf())
+                .post()
+                .uri{ uriBuilder: UriBuilder -> uriBuilder
+                        .path("/ott/generate")
+                        .build()
+                }
+                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                .body(BodyInserters.fromFormData("username", "user"))
+                .exchange()
+                .expectStatus()
+                .is3xxRedirection()
+                .expectHeader().valueEquals("Location", "/login/ott")
+
+        client.mutateWith(SecurityMockServerConfigurers.csrf())
+                .post()
+                .uri{ uriBuilder:UriBuilder -> uriBuilder
+                        .path("/ott/generate")
+                        .build()
+                }
+                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                .body(BodyInserters.fromFormData("username", "user"))
+                .exchange()
+                .expectStatus()
+                .is3xxRedirection()
+                .expectHeader().valueEquals("Location", "/login/ott")
+
+        val token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken
+
+        Assertions.assertThat(getCurrentMinutes(token!!.expiresAt)).isEqualTo(10)
+    }
+
+    private fun getCurrentMinutes(expiresAt:Instant): Int {
+        val expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).minute
+        val currentMinutes = Instant.now().atZone(ZoneOffset.UTC).minute
+        return expiresMinutes - currentMinutes
+    }
+
     @Configuration
     @EnableWebFlux
     @EnableWebFluxSecurity
@@ -199,6 +248,34 @@ class ServerOneTimeTokenLoginDslTests {
             MapReactiveUserDetailsService(User("user", "password", listOf()))
     }
 
+    @Configuration(proxyBeanMethods = false)
+    @EnableWebFlux
+    @EnableWebFluxSecurity
+    @Import(OneTimeTokenLoginSpecTests.UserDetailsServiceConfig::class)
+    open class OneTimeTokenConfigWithCustomTokenExpirationTime {
+        @Bean
+        open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            // @formatter:off
+            return http {
+                authorizeExchange {
+                    authorize(anyExchange, authenticated)
+                }
+                oneTimeTokenLogin {
+                    tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler()
+                }
+            }
+        }
+
+        @Bean
+        open fun resolver(): ServerGenerateOneTimeTokenRequestResolver {
+            val resolver = DefaultServerGenerateOneTimeTokenRequestResolver()
+            return ServerGenerateOneTimeTokenRequestResolver { exchange ->
+                resolver.resolve(exchange)
+                        .map { request -> GenerateOneTimeTokenRequest(request.username, Duration.ofSeconds(600)) }
+            }
+        }
+    }
+
     private class TestServerOneTimeTokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler {
         private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null
 

+ 32 - 0
docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc

@@ -546,3 +546,35 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender):
 
 ----
 ======
+
+[[customize-generate-token-request]]
+== Customize GenerateOneTimeTokenRequest Instance
+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.
+
+You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so:
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
+    DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
+    resolver.setExpiresIn(Duration.ofSeconds(600));
+    return resolver;
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver {
+    return DefaultServerGenerateOneTimeTokenRequestResolver().apply {
+        this.setExpiresIn(Duration.ofMinutes(10))
+    }
+}
+----
+======

+ 62 - 0
web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication.ott;
+
+import java.time.Duration;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Default implementation of {@link ServerGenerateOneTimeTokenRequestResolver}. Resolves
+ * {@link GenerateOneTimeTokenRequest} from username parameter.
+ *
+ * @author Max Batischev
+ * @since 6.5
+ */
+public final class DefaultServerGenerateOneTimeTokenRequestResolver
+		implements ServerGenerateOneTimeTokenRequestResolver {
+
+	private static final String USERNAME = "username";
+
+	private static final Duration DEFAULT_EXPIRES_IN = Duration.ofMinutes(5);
+
+	private Duration expiresIn = DEFAULT_EXPIRES_IN;
+
+	@Override
+	public Mono<GenerateOneTimeTokenRequest> resolve(ServerWebExchange exchange) {
+		// @formatter:off
+		return exchange.getFormData()
+				.mapNotNull((data) -> data.getFirst(USERNAME))
+				.switchIfEmpty(Mono.empty())
+				.map((username) -> new GenerateOneTimeTokenRequest(username, this.expiresIn));
+		// @formatter:on
+	}
+
+	/**
+	 * Sets one-time token expiration time
+	 * @param expiresIn one-time token expiration time
+	 */
+	public void setExpiresIn(Duration expiresIn) {
+		Assert.notNull(expiresIn, "expiresIn cannot be null");
+		this.expiresIn = expiresIn;
+	}
+
+}

+ 16 - 7
web/src/main/java/org/springframework/security/web/server/authentication/ott/GenerateOneTimeTokenWebFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package org.springframework.security.web.server.authentication.ott;
 import reactor.core.publisher.Mono;
 
 import org.springframework.http.HttpMethod;
-import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
 import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
@@ -37,12 +36,12 @@ import org.springframework.web.server.WebFilterChain;
  */
 public final class GenerateOneTimeTokenWebFilter implements WebFilter {
 
-	private static final String USERNAME = "username";
-
 	private final ReactiveOneTimeTokenService oneTimeTokenService;
 
 	private ServerWebExchangeMatcher matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/ott/generate");
 
+	private ServerGenerateOneTimeTokenRequestResolver generateRequestResolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
+
 	private final ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
 
 	public GenerateOneTimeTokenWebFilter(ReactiveOneTimeTokenService oneTimeTokenService,
@@ -58,10 +57,9 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter {
 		// @formatter:off
 		return this.matcher.matches(exchange)
 				.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
-				.then(exchange.getFormData())
-				.mapNotNull((data) -> data.getFirst(USERNAME))
+				.flatMap((result) -> this.generateRequestResolver.resolve(exchange))
 				.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
-				.flatMap((username) -> this.oneTimeTokenService.generate(new GenerateOneTimeTokenRequest(username)))
+				.flatMap(this.oneTimeTokenService::generate)
 				.flatMap((token) -> this.oneTimeTokenGenerationSuccessHandler.handle(exchange, token));
 		// @formatter:on
 	}
@@ -75,4 +73,15 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter {
 		this.matcher = matcher;
 	}
 
+	/**
+	 * Use the given {@link ServerGenerateOneTimeTokenRequestResolver} to resolve the
+	 * request, defaults to {@link DefaultServerGenerateOneTimeTokenRequestResolver}
+	 * @param requestResolver {@link ServerGenerateOneTimeTokenRequestResolver}
+	 * @since 6.5
+	 */
+	public void setGenerateRequestResolver(ServerGenerateOneTimeTokenRequestResolver requestResolver) {
+		Assert.notNull(requestResolver, "requestResolver cannot be null");
+		this.generateRequestResolver = requestResolver;
+	}
+
 }

+ 40 - 0
web/src/main/java/org/springframework/security/web/server/authentication/ott/ServerGenerateOneTimeTokenRequestResolver.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication.ott;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * A strategy for resolving a {@link GenerateOneTimeTokenRequest} from the
+ * {@link ServerWebExchange}.
+ *
+ * @author Max Batischev
+ * @since 6.5
+ */
+public interface ServerGenerateOneTimeTokenRequestResolver {
+
+	/**
+	 * Resolves {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}
+	 * @param exchange {@link ServerWebExchange} to resolve
+	 * @return {@link GenerateOneTimeTokenRequest}
+	 */
+	Mono<GenerateOneTimeTokenRequest> resolve(ServerWebExchange exchange);
+
+}

+ 74 - 0
web/src/test/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolverTests.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.authentication.ott;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.MediaType;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link DefaultServerGenerateOneTimeTokenRequestResolver}
+ *
+ * @author Max Batischev
+ */
+public class DefaultServerGenerateOneTimeTokenRequestResolverTests {
+
+	private final DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
+
+	@Test
+	void resolveWhenUsernameParameterIsPresentThenResolvesGenerateRequest() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/ott/generate")
+			.contentType(MediaType.APPLICATION_FORM_URLENCODED)
+			.body("username=user"));
+
+		GenerateOneTimeTokenRequest request = this.resolver.resolve(exchange).block();
+
+		assertThat(request).isNotNull();
+		assertThat(request.getUsername()).isEqualTo("user");
+		assertThat(request.getExpiresIn()).isEqualTo(Duration.ofMinutes(5));
+	}
+
+	@Test
+	void resolveWhenUsernameParameterIsNotPresentThenNull() {
+		MockServerWebExchange exchange = MockServerWebExchange
+			.from(MockServerHttpRequest.post("/ott/generate").contentType(MediaType.APPLICATION_FORM_URLENCODED));
+
+		GenerateOneTimeTokenRequest request = this.resolver.resolve(exchange).block();
+
+		assertThat(request).isNull();
+	}
+
+	@Test
+	void resolveWhenExpiresInSetThenResolvesGenerateRequest() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/ott/generate")
+			.contentType(MediaType.APPLICATION_FORM_URLENCODED)
+			.body("username=user"));
+		this.resolver.setExpiresIn(Duration.ofSeconds(600));
+
+		GenerateOneTimeTokenRequest generateRequest = this.resolver.resolve(exchange).block();
+
+		assertThat(generateRequest.getExpiresIn()).isEqualTo(Duration.ofSeconds(600));
+	}
+
+}