Skip to content

Commit dedf366

Browse files
Add Support GenerateOneTimeTokenRequestResolver
Closes gh-16291 Signed-off-by: Max Batischev <[email protected]>
1 parent 036f6f2 commit dedf366

File tree

11 files changed

+396
-49
lines changed

11 files changed

+396
-49
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,13 +18,15 @@
1818

1919
import java.util.Collections;
2020
import java.util.Map;
21+
import java.util.Objects;
2122

2223
import jakarta.servlet.http.HttpServletRequest;
2324

2425
import org.springframework.context.ApplicationContext;
2526
import org.springframework.http.HttpMethod;
2627
import org.springframework.security.authentication.AuthenticationManager;
2728
import org.springframework.security.authentication.AuthenticationProvider;
29+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
2830
import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
2931
import org.springframework.security.authentication.ott.OneTimeToken;
3032
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
@@ -40,7 +42,9 @@
4042
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
4143
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
4244
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
45+
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
4346
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
47+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
4448
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
4549
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
4650
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@@ -79,6 +83,8 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
7983

8084
private AuthenticationProvider authenticationProvider;
8185

86+
private GenerateOneTimeTokenRequestResolver requestResolver;
87+
8288
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
8389
this.context = context;
8490
}
@@ -135,6 +141,7 @@ private void configureOttGenerateFilter(H http) {
135141
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
136142
getOneTimeTokenGenerationSuccessHandler(http));
137143
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
144+
generateFilter.setRequestResolver(getGenerateRequestResolver(http));
138145
http.addFilter(postProcess(generateFilter));
139146
http.addFilter(DefaultResourcesFilter.css());
140147
}
@@ -301,6 +308,28 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
301308
return this.authenticationFailureHandler;
302309
}
303310

311+
/**
312+
* Use this {@link GenerateOneTimeTokenRequestResolver} when resolving
313+
* {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}. By default,
314+
* the {@link DefaultGenerateOneTimeTokenRequestResolver} is used.
315+
* @param requestResolver the {@link GenerateOneTimeTokenRequestResolver}
316+
* @since 6.5
317+
*/
318+
public OneTimeTokenLoginConfigurer<H> generateRequestResolver(GenerateOneTimeTokenRequestResolver requestResolver) {
319+
Assert.notNull(requestResolver, "requestResolver cannot be null");
320+
this.requestResolver = requestResolver;
321+
return this;
322+
}
323+
324+
private GenerateOneTimeTokenRequestResolver getGenerateRequestResolver(H http) {
325+
if (this.requestResolver != null) {
326+
return this.requestResolver;
327+
}
328+
GenerateOneTimeTokenRequestResolver bean = getBeanOrNull(http, GenerateOneTimeTokenRequestResolver.class);
329+
this.requestResolver = Objects.requireNonNullElseGet(bean, DefaultGenerateOneTimeTokenRequestResolver::new);
330+
return this.requestResolver;
331+
}
332+
304333
private OneTimeTokenService getOneTimeTokenService(H http) {
305334
if (this.oneTimeTokenService != null) {
306335
return this.oneTimeTokenService;

config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import org.springframework.security.config.annotation.web.configurers.ott.OneTim
2323
import org.springframework.security.web.authentication.AuthenticationConverter
2424
import org.springframework.security.web.authentication.AuthenticationFailureHandler
2525
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
26+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver
2627
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
2728

2829
/**
@@ -34,6 +35,7 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
3435
* @property authenticationConverter Use this [AuthenticationConverter] when converting incoming requests to an authentication
3536
* @property authenticationFailureHandler the [AuthenticationFailureHandler] to use when authentication
3637
* @property authenticationSuccessHandler the [AuthenticationSuccessHandler] to be used
38+
* @property generateRequestResolver the [GenerateOneTimeTokenRequestResolver] to be used
3739
* @property defaultSubmitPageUrl sets the URL that the default submit page will be generated
3840
* @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown
3941
* @property loginProcessingUrl the URL to process the login request
@@ -47,6 +49,7 @@ class OneTimeTokenLoginDsl {
4749
var authenticationConverter: AuthenticationConverter? = null
4850
var authenticationFailureHandler: AuthenticationFailureHandler? = null
4951
var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
52+
var generateRequestResolver: GenerateOneTimeTokenRequestResolver? = null
5053
var defaultSubmitPageUrl: String? = null
5154
var loginProcessingUrl: String? = null
5255
var tokenGeneratingUrl: String? = null
@@ -63,6 +66,11 @@ class OneTimeTokenLoginDsl {
6366
authenticationFailureHandler
6467
)
6568
}
69+
generateRequestResolver?.also {
70+
oneTimeTokenLoginConfigurer.generateRequestResolver(
71+
generateRequestResolver
72+
)
73+
}
6674
authenticationSuccessHandler?.also {
6775
oneTimeTokenLoginConfigurer.authenticationSuccessHandler(
6876
authenticationSuccessHandler

config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@
1717
package org.springframework.security.config.annotation.web.configurers.ott;
1818

1919
import java.io.IOException;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
import java.time.ZoneOffset;
2023

2124
import jakarta.servlet.ServletException;
2225
import jakarta.servlet.http.HttpServletRequest;
@@ -29,6 +32,7 @@
2932
import org.springframework.context.annotation.Bean;
3033
import org.springframework.context.annotation.Configuration;
3134
import org.springframework.context.annotation.Import;
35+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
3236
import org.springframework.security.authentication.ott.OneTimeToken;
3337
import org.springframework.security.config.Customizer;
3438
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -40,6 +44,8 @@
4044
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
4145
import org.springframework.security.web.SecurityFilterChain;
4246
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
47+
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
48+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
4349
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
4450
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
4551
import org.springframework.security.web.csrf.CsrfToken;
@@ -194,6 +200,55 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
194200
""");
195201
}
196202

203+
@Test
204+
void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception {
205+
this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire();
206+
this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf()))
207+
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
208+
209+
OneTimeToken token = TestOneTimeTokenGenerationSuccessHandler.lastToken;
210+
211+
this.mvc.perform(post("/login/ott").param("token", token.getTokenValue()).with(csrf()))
212+
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
213+
assertThat(getCurrentMinutes(token.getExpiresAt())).isEqualTo(10);
214+
}
215+
216+
private int getCurrentMinutes(Instant expiresAt) {
217+
int expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).getMinute();
218+
int currentMinutes = Instant.now().atZone(ZoneOffset.UTC).getMinute();
219+
return expiresMinutes - currentMinutes;
220+
}
221+
222+
@Configuration(proxyBeanMethods = false)
223+
@EnableWebSecurity
224+
@Import(UserDetailsServiceConfig.class)
225+
static class OneTimeTokenConfigWithCustomTokenExpirationTime {
226+
227+
@Bean
228+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
229+
// @formatter:off
230+
http
231+
.authorizeHttpRequests((authz) -> authz
232+
.anyRequest().authenticated()
233+
)
234+
.oneTimeTokenLogin((ott) -> ott
235+
.tokenGenerationSuccessHandler(new TestOneTimeTokenGenerationSuccessHandler())
236+
);
237+
// @formatter:on
238+
return http.build();
239+
}
240+
241+
@Bean
242+
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
243+
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
244+
return (request) -> {
245+
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
246+
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
247+
};
248+
}
249+
250+
}
251+
197252
@Configuration(proxyBeanMethods = false)
198253
@EnableWebSecurity
199254
@Import(UserDetailsServiceConfig.class)

0 commit comments

Comments
 (0)