Procházet zdrojové kódy

Add Support GenerateOneTimeTokenRequestResolver

Closes gh-16291

Signed-off-by: Max Batischev <mblancer@mail.ru>
Max Batischev před 7 měsíci
rodič
revize
474b5e151a
13 změnil soubory, kde provedl 398 přidání a 13 odebrání
  1. 30 1
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java
  2. 8 0
      config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt
  3. 56 1
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java
  4. 58 1
      config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt
  5. 17 1
      core/src/main/java/org/springframework/security/authentication/ott/GenerateOneTimeTokenRequest.java
  6. 3 3
      core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java
  7. 3 4
      core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java
  8. 34 0
      docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc
  9. 4 0
      docs/modules/ROOT/pages/whats-new.adoc
  10. 58 0
      web/src/main/java/org/springframework/security/web/authentication/ott/DefaultGenerateOneTimeTokenRequestResolver.java
  11. 19 2
      web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java
  12. 41 0
      web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenRequestResolver.java
  13. 67 0
      web/src/test/java/org/springframework/security/web/authentication/ott/DefaultGenerateOneTimeTokenRequestResolverTests.java

+ 30 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.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.
@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.configurers.ott;
 
 import java.util.Collections;
 import java.util.Map;
+import java.util.Objects;
 
 import jakarta.servlet.http.HttpServletRequest;
 
@@ -25,6 +26,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
 import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
 import org.springframework.security.authentication.ott.OneTimeToken;
 import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
@@ -40,7 +42,9 @@ import org.springframework.security.web.authentication.AuthenticationFilter;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
 import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
+import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
 import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
 import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
 import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@@ -79,6 +83,8 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
 
 	private AuthenticationProvider authenticationProvider;
 
+	private GenerateOneTimeTokenRequestResolver requestResolver;
+
 	public OneTimeTokenLoginConfigurer(ApplicationContext context) {
 		this.context = context;
 	}
@@ -135,6 +141,7 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
 		GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
 				getOneTimeTokenGenerationSuccessHandler(http));
 		generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
+		generateFilter.setRequestResolver(getGenerateRequestResolver(http));
 		http.addFilter(postProcess(generateFilter));
 		http.addFilter(DefaultResourcesFilter.css());
 	}
@@ -301,6 +308,28 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
 		return this.authenticationFailureHandler;
 	}
 
+	/**
+	 * Use this {@link GenerateOneTimeTokenRequestResolver} when resolving
+	 * {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}. By default,
+	 * the {@link DefaultGenerateOneTimeTokenRequestResolver} is used.
+	 * @param requestResolver the {@link GenerateOneTimeTokenRequestResolver}
+	 * @since 6.5
+	 */
+	public OneTimeTokenLoginConfigurer<H> generateRequestResolver(GenerateOneTimeTokenRequestResolver requestResolver) {
+		Assert.notNull(requestResolver, "requestResolver cannot be null");
+		this.requestResolver = requestResolver;
+		return this;
+	}
+
+	private GenerateOneTimeTokenRequestResolver getGenerateRequestResolver(H http) {
+		if (this.requestResolver != null) {
+			return this.requestResolver;
+		}
+		GenerateOneTimeTokenRequestResolver bean = getBeanOrNull(http, GenerateOneTimeTokenRequestResolver.class);
+		this.requestResolver = Objects.requireNonNullElseGet(bean, DefaultGenerateOneTimeTokenRequestResolver::new);
+		return this.requestResolver;
+	}
+
 	private OneTimeTokenService getOneTimeTokenService(H http) {
 		if (this.oneTimeTokenService != null) {
 			return this.oneTimeTokenService;

+ 8 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt

@@ -23,6 +23,7 @@ import org.springframework.security.config.annotation.web.configurers.ott.OneTim
 import org.springframework.security.web.authentication.AuthenticationConverter
 import org.springframework.security.web.authentication.AuthenticationFailureHandler
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler
+import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver
 import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
 
 /**
@@ -34,6 +35,7 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
  * @property authenticationConverter Use this [AuthenticationConverter] when converting incoming requests to an authentication
  * @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication
  * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] to be used
+ * @property generateRequestResolver the [GenerateOneTimeTokenRequestResolver] 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
@@ -47,6 +49,7 @@ class OneTimeTokenLoginDsl {
     var authenticationConverter: AuthenticationConverter? = null
     var authenticationFailureHandler: AuthenticationFailureHandler? = null
     var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
+    var generateRequestResolver: GenerateOneTimeTokenRequestResolver? = null
     var defaultSubmitPageUrl: String? = null
     var loginProcessingUrl: String? = null
     var tokenGeneratingUrl: String? = null
@@ -68,6 +71,11 @@ class OneTimeTokenLoginDsl {
                     authenticationSuccessHandler
                 )
             }
+            generateRequestResolver?.also {
+                oneTimeTokenLoginConfigurer.generateRequestResolver(
+                        generateRequestResolver
+                )
+            }
             defaultSubmitPageUrl?.also { oneTimeTokenLoginConfigurer.defaultSubmitPageUrl(defaultSubmitPageUrl) }
             showDefaultSubmitPage?.also { oneTimeTokenLoginConfigurer.showDefaultSubmitPage(showDefaultSubmitPage!!) }
             loginProcessingUrl?.also { oneTimeTokenLoginConfigurer.loginProcessingUrl(loginProcessingUrl) }

+ 56 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.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.
@@ -17,6 +17,9 @@
 package org.springframework.security.config.annotation.web.configurers.ott;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
 
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
@@ -29,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
 import org.springframework.security.authentication.ott.OneTimeToken;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -40,6 +44,8 @@ import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
+import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
 import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
 import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
 import org.springframework.security.web.csrf.CsrfToken;
@@ -194,6 +200,55 @@ public class OneTimeTokenLoginConfigurerTests {
 					""");
 	}
 
+	@Test
+	void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception {
+		this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire();
+		this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf()))
+			.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
+
+		OneTimeToken token = TestOneTimeTokenGenerationSuccessHandler.lastToken;
+
+		this.mvc.perform(post("/login/ott").param("token", token.getTokenValue()).with(csrf()))
+			.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
+		assertThat(getCurrentMinutes(token.getExpiresAt())).isEqualTo(10);
+	}
+
+	private int getCurrentMinutes(Instant expiresAt) {
+		int expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).getMinute();
+		int currentMinutes = Instant.now().atZone(ZoneOffset.UTC).getMinute();
+		return expiresMinutes - currentMinutes;
+	}
+
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebSecurity
+	@Import(UserDetailsServiceConfig.class)
+	static class OneTimeTokenConfigWithCustomTokenExpirationTime {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.authorizeHttpRequests((authz) -> authz
+							.anyRequest().authenticated()
+					)
+					.oneTimeTokenLogin((ott) -> ott
+							.tokenGenerationSuccessHandler(new TestOneTimeTokenGenerationSuccessHandler())
+					);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
+			DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
+			return (request) -> {
+				GenerateOneTimeTokenRequest generate = delegate.resolve(request);
+				return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
+			};
+		}
+
+	}
+
 	@Configuration(proxyBeanMethods = false)
 	@EnableWebSecurity
 	@Import(UserDetailsServiceConfig.class)

+ 58 - 1
config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.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.annotation.web
 
 import jakarta.servlet.http.HttpServletRequest
 import jakarta.servlet.http.HttpServletResponse
+import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
 import org.springframework.beans.factory.annotation.Autowired
@@ -36,11 +37,15 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ
 import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers
 import org.springframework.security.web.SecurityFilterChain
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
+import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver
 import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
 import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
 import org.springframework.test.web.servlet.MockMvc
 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
 import org.springframework.test.web.servlet.result.MockMvcResultMatchers
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneOffset
 
 /**
  * Tests for [OneTimeTokenLoginDsl]
@@ -104,6 +109,32 @@ class OneTimeTokenLoginDslTests {
             )
     }
 
+    @Test
+    fun `oneTimeToken when custom resolver set then use custom token`() {
+        spring.register(OneTimeTokenConfigWithCustomTokenResolver::class.java).autowire()
+
+        this.mockMvc.perform(
+                MockMvcRequestBuilders.post("/ott/generate").param("username", "user")
+                        .with(SecurityMockMvcRequestPostProcessors.csrf())
+        ).andExpectAll(
+                MockMvcResultMatchers
+                        .status()
+                        .isFound(),
+                MockMvcResultMatchers
+                        .redirectedUrl("/login/ott")
+        )
+
+        val token = TestOneTimeTokenGenerationSuccessHandler.lastToken
+
+        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
     @EnableWebSecurity
     @Import(UserDetailsServiceConfig::class)
@@ -125,6 +156,32 @@ class OneTimeTokenLoginDslTests {
         }
     }
 
+    @Configuration
+    @EnableWebSecurity
+    @Import(UserDetailsServiceConfig::class)
+    open class OneTimeTokenConfigWithCustomTokenResolver {
+
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            // @formatter:off
+            http {
+                authorizeHttpRequests {
+                    authorize(anyRequest, authenticated)
+                }
+                oneTimeTokenLogin {
+                    oneTimeTokenGenerationSuccessHandler = TestOneTimeTokenGenerationSuccessHandler()
+                    generateRequestResolver = DefaultGenerateOneTimeTokenRequestResolver().apply {
+                        this.setExpiresIn(Duration.ofMinutes(10))
+                    }
+                }
+            }
+            // @formatter:on
+            return http.build()
+        }
+
+
+    }
+
     @EnableWebSecurity
     @Configuration(proxyBeanMethods = false)
     @Import(UserDetailsServiceConfig::class)

+ 17 - 1
core/src/main/java/org/springframework/security/authentication/ott/GenerateOneTimeTokenRequest.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.
@@ -16,6 +16,8 @@
 
 package org.springframework.security.authentication.ott;
 
+import java.time.Duration;
+
 import org.springframework.util.Assert;
 
 /**
@@ -26,15 +28,29 @@ import org.springframework.util.Assert;
  */
 public class GenerateOneTimeTokenRequest {
 
+	private static final Duration DEFAULT_EXPIRES_IN = Duration.ofMinutes(5);
+
 	private final String username;
 
+	private final Duration expiresIn;
+
 	public GenerateOneTimeTokenRequest(String username) {
+		this(username, DEFAULT_EXPIRES_IN);
+	}
+
+	public GenerateOneTimeTokenRequest(String username, Duration expiresIn) {
 		Assert.hasText(username, "username cannot be empty");
+		Assert.notNull(expiresIn, "expiresIn cannot be null");
 		this.username = username;
+		this.expiresIn = expiresIn;
 	}
 
 	public String getUsername() {
 		return this.username;
 	}
 
+	public Duration getExpiresIn() {
+		return this.expiresIn;
+	}
+
 }

+ 3 - 3
core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.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.
@@ -44,8 +44,8 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
 	@NonNull
 	public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
 		String token = UUID.randomUUID().toString();
-		Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
-		OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
+		Instant expiresAt = this.clock.instant().plus(request.getExpiresIn());
+		OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
 		this.oneTimeTokenByToken.put(token, ott);
 		cleanExpiredTokensIfNeeded();
 		return ott;

+ 3 - 4
core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.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.
@@ -21,7 +21,6 @@ import java.sql.SQLException;
 import java.sql.Timestamp;
 import java.sql.Types;
 import java.time.Clock;
-import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
@@ -132,8 +131,8 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
 	public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
 		Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
 		String token = UUID.randomUUID().toString();
-		Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
-		OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
+		Instant expiresAt = this.clock.instant().plus(request.getExpiresIn());
+		OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
 		insertOneTimeToken(oneTimeToken);
 		return oneTimeToken;
 	}

+ 34 - 0
docs/modules/ROOT/pages/servlet/authentication/onetimetoken.adoc

@@ -545,3 +545,37 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSucc
 }
 ----
 ======
+
+[[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 GenerateOneTimeTokenRequestResolver as a @Bean, like so:
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
+    DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
+        return (request) -> {
+		    GenerateOneTimeTokenRequest generate = delegate.resolve(request);
+		    return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
+	};
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
+    return DefaultGenerateOneTimeTokenRequestResolver().apply {
+        this.setExpiresIn(Duration.ofMinutes(10))
+    }
+}
+----
+======

+ 4 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -20,3 +20,7 @@ Note that this may affect reports that operate on this key name.
 * https://github.com/spring-projects/spring-security/pull/16282[gh-16282] - xref:servlet/authentication/passkeys.adoc#passkeys-configuration-persistence[JDBC Persistence] for WebAuthn/Passkeys
 * https://github.com/spring-projects/spring-security/pull/16397[gh-16397] - Added the ability to configure a custom `HttpMessageConverter` for Passkeys using the optional xref:servlet/authentication/passkeys.adoc#passkeys-configuration[`messageConverter` property] on the `webAuthn` DSL.
 * https://github.com/spring-projects/spring-security/pull/16396[gh-16396] - Added the ability to configure a custom xref:servlet/authentication/passkeys.adoc#passkeys-configuration-pkccor[`PublicKeyCredentialCreationOptionsRepository`]
+
+== One-Time Token Login
+
+* https://github.com/spring-projects/spring-security/issues/16291[gh-16291] - `oneTimeTokenLogin()` now supports customizing GenerateOneTimeTokenRequest xref:servlet/authentication/onetimetoken.adoc#customize-generate-token-request[via GenerateOneTimeTokenRequestResolver]

+ 58 - 0
web/src/main/java/org/springframework/security/web/authentication/ott/DefaultGenerateOneTimeTokenRequestResolver.java

@@ -0,0 +1,58 @@
+/*
+ * 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.authentication.ott;
+
+import java.time.Duration;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Default implementation of {@link GenerateOneTimeTokenRequestResolver}. Resolves
+ * {@link GenerateOneTimeTokenRequest} from username parameter.
+ *
+ * @author Max Batischev
+ * @since 6.5
+ */
+public final class DefaultGenerateOneTimeTokenRequestResolver implements GenerateOneTimeTokenRequestResolver {
+
+	private static final Duration DEFAULT_EXPIRES_IN = Duration.ofMinutes(5);
+
+	private Duration expiresIn = DEFAULT_EXPIRES_IN;
+
+	@Override
+	public GenerateOneTimeTokenRequest resolve(HttpServletRequest request) {
+		String username = request.getParameter("username");
+		if (!StringUtils.hasText(username)) {
+			return null;
+		}
+		return new GenerateOneTimeTokenRequest(username, this.expiresIn);
+	}
+
+	/**
+	 * Sets one-time token expiration time
+	 * @param expiresIn one-time token expiration time
+	 */
+	public void setExpiresIn(Duration expiresIn) {
+		Assert.notNull(expiresIn, "expiresAt cannot be null");
+		this.expiresIn = expiresIn;
+	}
+
+}

+ 19 - 2
web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.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.
@@ -49,6 +49,8 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
 
 	private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate");
 
+	private GenerateOneTimeTokenRequestResolver requestResolver = new DefaultGenerateOneTimeTokenRequestResolver();
+
 	public GenerateOneTimeTokenFilter(OneTimeTokenService tokenService,
 			OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler) {
 		Assert.notNull(tokenService, "tokenService cannot be null");
@@ -69,8 +71,12 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
 			filterChain.doFilter(request, response);
 			return;
 		}
-		GenerateOneTimeTokenRequest generateRequest = new GenerateOneTimeTokenRequest(username);
+		GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
 		OneTimeToken ott = this.tokenService.generate(generateRequest);
+		if (generateRequest == null) {
+			filterChain.doFilter(request, response);
+			return;
+		}
 		this.tokenGenerationSuccessHandler.handle(request, response, ott);
 	}
 
@@ -83,4 +89,15 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
 		this.requestMatcher = requestMatcher;
 	}
 
+	/**
+	 * Use the given {@link GenerateOneTimeTokenRequestResolver} to resolve
+	 * {@link GenerateOneTimeTokenRequest}.
+	 * @param requestResolver {@link GenerateOneTimeTokenRequestResolver}
+	 * @since 6.5
+	 */
+	public void setRequestResolver(GenerateOneTimeTokenRequestResolver requestResolver) {
+		Assert.notNull(requestResolver, "requestResolver cannot be null");
+		this.requestResolver = requestResolver;
+	}
+
 }

+ 41 - 0
web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenRequestResolver.java

@@ -0,0 +1,41 @@
+/*
+ * 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.authentication.ott;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+
+/**
+ * A strategy for resolving a {@link GenerateOneTimeTokenRequest} from the
+ * {@link HttpServletRequest}.
+ *
+ * @author Max Batischev
+ * @since 6.5
+ */
+public interface GenerateOneTimeTokenRequestResolver {
+
+	/**
+	 * Resolves {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}
+	 * @param request {@link HttpServletRequest} to resolve
+	 * @return {@link GenerateOneTimeTokenRequest}
+	 */
+	@Nullable
+	GenerateOneTimeTokenRequest resolve(HttpServletRequest request);
+
+}

+ 67 - 0
web/src/test/java/org/springframework/security/web/authentication/ott/DefaultGenerateOneTimeTokenRequestResolverTests.java

@@ -0,0 +1,67 @@
+/*
+ * 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.authentication.ott;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link DefaultGenerateOneTimeTokenRequestResolver}
+ *
+ * @author Max Batischev
+ */
+public class DefaultGenerateOneTimeTokenRequestResolverTests {
+
+	private final DefaultGenerateOneTimeTokenRequestResolver requestResolver = new DefaultGenerateOneTimeTokenRequestResolver();
+
+	@Test
+	void resolveWhenUsernameParameterIsPresentThenResolvesGenerateRequest() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setParameter("username", "test");
+
+		GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
+
+		assertThat(generateRequest).isNotNull();
+		assertThat(generateRequest.getUsername()).isEqualTo("test");
+		assertThat(generateRequest.getExpiresIn()).isEqualTo(Duration.ofSeconds(300));
+	}
+
+	@Test
+	void resolveWhenUsernameParameterIsNotPresentThenNull() {
+		GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(new MockHttpServletRequest());
+
+		assertThat(generateRequest).isNull();
+	}
+
+	@Test
+	void resolveWhenExpiresInSetThenResolvesGenerateRequest() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setParameter("username", "test");
+		this.requestResolver.setExpiresIn(Duration.ofSeconds(600));
+
+		GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
+
+		assertThat(generateRequest.getExpiresIn()).isEqualTo(Duration.ofSeconds(600));
+	}
+
+}