mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-26 13:53:14 +00:00
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
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.InMemoryOneTimeTokenService;
|
||||||
import org.springframework.security.authentication.ott.OneTimeToken;
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
|
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.AuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
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.GenerateOneTimeTokenFilter;
|
||||||
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
|
||||||
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
|
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
|
||||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||||
@ -79,6 +83,8 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
|
|
||||||
private AuthenticationProvider authenticationProvider;
|
private AuthenticationProvider authenticationProvider;
|
||||||
|
|
||||||
|
private GenerateOneTimeTokenRequestResolver requestResolver;
|
||||||
|
|
||||||
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
|
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
@ -135,6 +141,7 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
|
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
|
||||||
getOneTimeTokenGenerationSuccessHandler(http));
|
getOneTimeTokenGenerationSuccessHandler(http));
|
||||||
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
|
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
|
||||||
|
generateFilter.setRequestResolver(getGenerateRequestResolver(http));
|
||||||
http.addFilter(postProcess(generateFilter));
|
http.addFilter(postProcess(generateFilter));
|
||||||
http.addFilter(DefaultResourcesFilter.css());
|
http.addFilter(DefaultResourcesFilter.css());
|
||||||
}
|
}
|
||||||
@ -301,6 +308,28 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
return this.authenticationFailureHandler;
|
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) {
|
private OneTimeTokenService getOneTimeTokenService(H http) {
|
||||||
if (this.oneTimeTokenService != null) {
|
if (this.oneTimeTokenService != null) {
|
||||||
return this.oneTimeTokenService;
|
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.AuthenticationConverter
|
||||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
||||||
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver
|
||||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
|
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 authenticationConverter Use this [AuthenticationConverter] when converting incoming requests to an authentication
|
||||||
* @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication
|
* @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication
|
||||||
* @property authenticationSuccessHandler the [AuthenticationSuccessHandler] to be used
|
* @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 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 showDefaultSubmitPage configures whether the default one-time token submit page should be shown
|
||||||
* @property loginProcessingUrl the URL to process the login request
|
* @property loginProcessingUrl the URL to process the login request
|
||||||
@ -47,6 +49,7 @@ class OneTimeTokenLoginDsl {
|
|||||||
var authenticationConverter: AuthenticationConverter? = null
|
var authenticationConverter: AuthenticationConverter? = null
|
||||||
var authenticationFailureHandler: AuthenticationFailureHandler? = null
|
var authenticationFailureHandler: AuthenticationFailureHandler? = null
|
||||||
var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
|
var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
|
||||||
|
var generateRequestResolver: GenerateOneTimeTokenRequestResolver? = null
|
||||||
var defaultSubmitPageUrl: String? = null
|
var defaultSubmitPageUrl: String? = null
|
||||||
var loginProcessingUrl: String? = null
|
var loginProcessingUrl: String? = null
|
||||||
var tokenGeneratingUrl: String? = null
|
var tokenGeneratingUrl: String? = null
|
||||||
@ -68,6 +71,11 @@ class OneTimeTokenLoginDsl {
|
|||||||
authenticationSuccessHandler
|
authenticationSuccessHandler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
generateRequestResolver?.also {
|
||||||
|
oneTimeTokenLoginConfigurer.generateRequestResolver(
|
||||||
|
generateRequestResolver
|
||||||
|
)
|
||||||
|
}
|
||||||
defaultSubmitPageUrl?.also { oneTimeTokenLoginConfigurer.defaultSubmitPageUrl(defaultSubmitPageUrl) }
|
defaultSubmitPageUrl?.also { oneTimeTokenLoginConfigurer.defaultSubmitPageUrl(defaultSubmitPageUrl) }
|
||||||
showDefaultSubmitPage?.also { oneTimeTokenLoginConfigurer.showDefaultSubmitPage(showDefaultSubmitPage!!) }
|
showDefaultSubmitPage?.also { oneTimeTokenLoginConfigurer.showDefaultSubmitPage(showDefaultSubmitPage!!) }
|
||||||
loginProcessingUrl?.also { oneTimeTokenLoginConfigurer.loginProcessingUrl(loginProcessingUrl) }
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
package org.springframework.security.config.annotation.web.configurers.ott;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
|
||||||
import org.springframework.security.authentication.ott.OneTimeToken;
|
import org.springframework.security.authentication.ott.OneTimeToken;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
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.OneTimeTokenGenerationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
|
||||||
import org.springframework.security.web.csrf.CsrfToken;
|
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)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Import(UserDetailsServiceConfig.class)
|
@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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.HttpServletRequest
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
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.test.web.servlet.response.SecurityMockMvcResultMatchers
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
|
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.OneTimeTokenGenerationSuccessHandler
|
||||||
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
|
||||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for [OneTimeTokenLoginDsl]
|
* 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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Import(UserDetailsServiceConfig::class)
|
@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
|
@EnableWebSecurity
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@Import(UserDetailsServiceConfig::class)
|
@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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.springframework.security.authentication.ott;
|
package org.springframework.security.authentication.ott;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,15 +28,29 @@ import org.springframework.util.Assert;
|
|||||||
*/
|
*/
|
||||||
public class GenerateOneTimeTokenRequest {
|
public class GenerateOneTimeTokenRequest {
|
||||||
|
|
||||||
|
private static final Duration DEFAULT_EXPIRES_IN = Duration.ofMinutes(5);
|
||||||
|
|
||||||
private final String username;
|
private final String username;
|
||||||
|
|
||||||
|
private final Duration expiresIn;
|
||||||
|
|
||||||
public GenerateOneTimeTokenRequest(String username) {
|
public GenerateOneTimeTokenRequest(String username) {
|
||||||
|
this(username, DEFAULT_EXPIRES_IN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerateOneTimeTokenRequest(String username, Duration expiresIn) {
|
||||||
Assert.hasText(username, "username cannot be empty");
|
Assert.hasText(username, "username cannot be empty");
|
||||||
|
Assert.notNull(expiresIn, "expiresIn cannot be null");
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
this.expiresIn = expiresIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return this.username;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
@NonNull
|
||||||
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
|
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
|
||||||
String token = UUID.randomUUID().toString();
|
String token = UUID.randomUUID().toString();
|
||||||
Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
|
Instant expiresAt = this.clock.instant().plus(request.getExpiresIn());
|
||||||
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
|
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
|
||||||
this.oneTimeTokenByToken.put(token, ott);
|
this.oneTimeTokenByToken.put(token, ott);
|
||||||
cleanExpiredTokensIfNeeded();
|
cleanExpiredTokensIfNeeded();
|
||||||
return ott;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Timestamp;
|
||||||
import java.sql.Types;
|
import java.sql.Types;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -132,8 +131,8 @@ public final class JdbcOneTimeTokenService implements OneTimeTokenService, Dispo
|
|||||||
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
|
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
|
||||||
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
|
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
|
||||||
String token = UUID.randomUUID().toString();
|
String token = UUID.randomUUID().toString();
|
||||||
Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
|
Instant expiresAt = this.clock.instant().plus(request.getExpiresIn());
|
||||||
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
|
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
|
||||||
insertOneTimeToken(oneTimeToken);
|
insertOneTimeToken(oneTimeToken);
|
||||||
return 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/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/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`]
|
* 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate");
|
||||||
|
|
||||||
|
private GenerateOneTimeTokenRequestResolver requestResolver = new DefaultGenerateOneTimeTokenRequestResolver();
|
||||||
|
|
||||||
public GenerateOneTimeTokenFilter(OneTimeTokenService tokenService,
|
public GenerateOneTimeTokenFilter(OneTimeTokenService tokenService,
|
||||||
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler) {
|
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler) {
|
||||||
Assert.notNull(tokenService, "tokenService cannot be null");
|
Assert.notNull(tokenService, "tokenService cannot be null");
|
||||||
@ -69,8 +71,12 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GenerateOneTimeTokenRequest generateRequest = new GenerateOneTimeTokenRequest(username);
|
GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
|
||||||
OneTimeToken ott = this.tokenService.generate(generateRequest);
|
OneTimeToken ott = this.tokenService.generate(generateRequest);
|
||||||
|
if (generateRequest == null) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.tokenGenerationSuccessHandler.handle(request, response, ott);
|
this.tokenGenerationSuccessHandler.handle(request, response, ott);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,4 +89,15 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
|
|||||||
this.requestMatcher = requestMatcher;
|
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…
x
Reference in New Issue
Block a user