Add Support GenerateOneTimeTokenRequestResolver
Closes gh-16291 Signed-off-by: Max Batischev <mblancer@mail.ru>
This commit is contained in:
		
							parent
							
								
									68c8a5ad99
								
							
						
					
					
						commit
						474b5e151a
					
				|  | @ -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; | ||||
|  |  | |||
|  | @ -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) } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
| 	} | ||||
|  |  | |||
|  | @ -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)) | ||||
|     } | ||||
| } | ||||
| ---- | ||||
| ====== | ||||
|  |  | |||
|  | @ -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] | ||||
|  |  | |||
|  | @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
| } | ||||
|  | @ -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)); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
		Loading…
	
		Reference in New Issue