Add Support GenerateOneTimeTokenRequestResolver
Closes gh-16291 Signed-off-by: Max Batischev <mblancer@mail.ru>
This commit is contained in:
+30
-1
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user