One Time Token login registers the default login page

closes gh-16414

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2025-01-22 11:33:10 +01:00 committed by Rob Winch
parent 5ee6b83953
commit 238f47ce5e
10 changed files with 242 additions and 127 deletions

View File

@ -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.

View File

@ -23,7 +23,6 @@ import jakarta.servlet.http.HttpServletRequest;
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;
@ -32,6 +31,9 @@ import org.springframework.security.authentication.ott.OneTimeTokenAuthenticatio
import org.springframework.security.authentication.ott.OneTimeTokenService;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
@ -49,34 +51,70 @@ import org.springframework.security.web.authentication.ott.OneTimeTokenGeneratio
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<OneTimeTokenLoginConfigurer<H>, H> {
/**
* An {@link AbstractHttpConfigurer} for One-Time Token Login.
*
* <p>
* One-Time Token Login provides an application with the capability to have users log in
* by obtaining a single-use token out of band, for example through email.
*
* <p>
* Defaults are provided for all configuration options, with the only required
* configuration being
* {@link #tokenGenerationSuccessHandler(OneTimeTokenGenerationSuccessHandler)}.
* Alternatively, a {@link OneTimeTokenGenerationSuccessHandler} {@code @Bean} may be
* registered instead.
*
* <h2>Security Filters</h2>
*
* The following {@code Filter}s are populated:
*
* <ul>
* <li>{@link DefaultOneTimeTokenSubmitPageGeneratingFilter}</li>
* <li>{@link GenerateOneTimeTokenFilter}</li>
* <li>{@link OneTimeTokenAuthenticationFilter}</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
* login page will be made available</li>
* </ul>
*
* @author Marcus Da Coregio
* @author Daniel Garnier-Moiroux
* @since 6.4
* @see HttpSecurity#oneTimeTokenLogin(Customizer)
* @see DefaultOneTimeTokenSubmitPageGeneratingFilter
* @see GenerateOneTimeTokenFilter
* @see OneTimeTokenAuthenticationFilter
* @see AbstractAuthenticationFilterConfigurer
*/
public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, OneTimeTokenLoginConfigurer<H>, OneTimeTokenAuthenticationFilter> {
private final ApplicationContext context;
private OneTimeTokenService oneTimeTokenService;
private AuthenticationConverter authenticationConverter = new OneTimeTokenAuthenticationConverter();
private AuthenticationFailureHandler authenticationFailureHandler;
private AuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private String defaultSubmitPageUrl = "/login/ott";
private String defaultSubmitPageUrl = DefaultOneTimeTokenSubmitPageGeneratingFilter.DEFAULT_SUBMIT_PAGE_URL;
private boolean submitPageEnabled = true;
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
private String tokenGeneratingUrl = "/ott/generate";
private String tokenGeneratingUrl = GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL;
private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
@ -85,58 +123,41 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
private GenerateOneTimeTokenRequestResolver requestResolver;
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
super(new OneTimeTokenAuthenticationFilter(), OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL);
this.context = context;
}
@Override
public void init(H http) {
public void init(H http) throws Exception {
super.init(http);
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
http.authenticationProvider(postProcess(authenticationProvider));
configureDefaultLoginPage(http);
intiDefaultLoginFilter(http);
}
private void configureDefaultLoginPage(H http) {
private void intiDefaultLoginFilter(H http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter == null) {
if (loginPageGeneratingFilter == null || isCustomLoginPage()) {
return;
}
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
loginPageGeneratingFilter.setOneTimeTokenGenerationUrl(this.tokenGeneratingUrl);
if (this.authenticationFailureHandler == null
&& StringUtils.hasText(loginPageGeneratingFilter.getLoginPageUrl())) {
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler(
loginPageGeneratingFilter.getLoginPageUrl() + "?error");
if (!StringUtils.hasText(loginPageGeneratingFilter.getLoginPageUrl())) {
loginPageGeneratingFilter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
loginPageGeneratingFilter.setFailureUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL + "?"
+ DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME);
loginPageGeneratingFilter
.setLogoutSuccessUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL + "?logout");
}
}
@Override
public void configure(H http) {
public void configure(H http) throws Exception {
super.configure(http);
configureSubmitPage(http);
configureOttGenerateFilter(http);
configureOttAuthenticationFilter(http);
}
private void configureOttAuthenticationFilter(H http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter();
oneTimeTokenAuthenticationFilter.setAuthenticationManager(authenticationManager);
if (this.loginProcessingUrl != null) {
oneTimeTokenAuthenticationFilter
.setRequiresAuthenticationRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
}
oneTimeTokenAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
oneTimeTokenAuthenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
}
private SecurityContextRepository getSecurityContextRepository(H http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
if (securityContextRepository != null) {
return securityContextRepository;
}
return new HttpSessionSecurityContextRepository();
}
private void configureOttGenerateFilter(H http) {
@ -170,7 +191,7 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter();
submitPage.setResolveHiddenInputs(this::hiddenInputs);
submitPage.setRequestMatcher(antMatcher(HttpMethod.GET, this.defaultSubmitPageUrl));
submitPage.setLoginProcessingUrl(this.loginProcessingUrl);
submitPage.setLoginProcessingUrl(this.getLoginProcessingUrl());
http.addFilter(postProcess(submitPage));
}
@ -184,6 +205,11 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
return this.authenticationProvider;
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return antMatcher(HttpMethod.POST, loginProcessingUrl);
}
/**
* Specifies the {@link AuthenticationProvider} to use when authenticating the user.
* @param authenticationProvider
@ -221,14 +247,25 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
* Only POST requests are processed, for that reason make sure that you pass a valid
* CSRF token if CSRF protection is enabled.
* @param loginProcessingUrl
* @see org.springframework.security.config.annotation.web.builders.HttpSecurity#csrf(Customizer)
* @see HttpSecurity#csrf(Customizer)
*/
public OneTimeTokenLoginConfigurer<H> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
this.loginProcessingUrl = loginProcessingUrl;
super.loginProcessingUrl(loginProcessingUrl);
return this;
}
/**
* Specifies the URL to send users to if login is required. If used with
* {@link EnableWebSecurity} a default login page will be generated when this
* attribute is not specified.
* @param loginPage
*/
@Override
public OneTimeTokenLoginConfigurer<H> loginPage(String loginPage) {
return super.loginPage(loginPage);
}
/**
* Configures whether the default one-time token submit page should be shown. This
* will prevent the {@link DefaultOneTimeTokenSubmitPageGeneratingFilter} to be
@ -273,7 +310,7 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
*/
public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
this.getAuthenticationFilter().setAuthenticationConverter(authenticationConverter);
return this;
}
@ -283,11 +320,13 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
* {@link SimpleUrlAuthenticationFailureHandler}
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
* when authentication fails.
* @deprecated Use {@link #failureHandler(AuthenticationFailureHandler)} instead
*/
@Deprecated(since = "6.5")
public OneTimeTokenLoginConfigurer<H> authenticationFailureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
super.failureHandler(authenticationFailureHandler);
return this;
}
@ -296,22 +335,16 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
* set.
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler}.
* @deprecated Use {@link #successHandler(AuthenticationSuccessHandler)} instead
*/
@Deprecated(since = "6.5")
public OneTimeTokenLoginConfigurer<H> authenticationSuccessHandler(
AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
super.successHandler(authenticationSuccessHandler);
return this;
}
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
if (this.authenticationFailureHandler != null) {
return this.authenticationFailureHandler;
}
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");
return this.authenticationFailureHandler;
}
/**
* Use this {@link GenerateOneTimeTokenRequestResolver} when resolving
* {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}. By default,

View File

@ -3039,7 +3039,8 @@ public class ServerHttpSecurity {
return;
}
if (http.formLogin != null && http.formLogin.isEntryPointExplicit
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) {
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)
|| http.oneTimeTokenLogin != null && StringUtils.hasText(http.oneTimeTokenLogin.loginPage)) {
return;
}
LoginPageGeneratingWebFilter loginPage = null;
@ -3054,6 +3055,13 @@ public class ServerHttpSecurity {
}
loginPage.setOauth2AuthenticationUrlToClientName(urlToText);
}
if (http.oneTimeTokenLogin != null) {
if (loginPage == null) {
loginPage = new LoginPageGeneratingWebFilter();
}
loginPage.setOneTimeTokenEnabled(true);
loginPage.setGenerateOneTimeTokenUrl(http.oneTimeTokenLogin.tokenGeneratingUrl);
}
if (loginPage != null) {
http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
http.addFilterBefore(DefaultResourcesWebFilter.css(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
@ -5954,11 +5962,13 @@ public class ServerHttpSecurity {
private boolean submitPageEnabled = true;
private String loginPage;
protected void configure(ServerHttpSecurity http) {
configureSubmitPage(http);
configureOttGenerateFilter(http);
configureOttAuthenticationFilter(http);
configureDefaultLoginPage(http);
configureDefaultEntryPoint(http);
}
private void configureOttAuthenticationFilter(ServerHttpSecurity http) {
@ -5995,17 +6005,29 @@ public class ServerHttpSecurity {
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
}
private void configureDefaultLoginPage(ServerHttpSecurity http) {
if (http.formLogin != null) {
for (WebFilter webFilter : http.webFilters) {
OrderedWebFilter orderedWebFilter = (OrderedWebFilter) webFilter;
if (orderedWebFilter.webFilter instanceof LoginPageGeneratingWebFilter loginPageGeneratingFilter) {
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
loginPageGeneratingFilter.setGenerateOneTimeTokenUrl(this.tokenGeneratingUrl);
break;
}
private void configureDefaultEntryPoint(ServerHttpSecurity http) {
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
MediaType.TEXT_PLAIN);
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
ServerWebExchangeMatcher xhrMatcher = (exchange) -> {
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
return ServerWebExchangeMatcher.MatchResult.match();
}
return ServerWebExchangeMatcher.MatchResult.notMatch();
};
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
htmlMatcher);
String loginPage = "/login";
if (this.loginPage != null) {
loginPage = this.loginPage;
}
RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
loginPage);
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
}
/**
@ -6233,6 +6255,19 @@ public class ServerHttpSecurity {
return this.tokenGenerationSuccessHandler;
}
/**
* Specifies the URL to send users to if login is required. A default login page
* will be generated when this attribute is not specified.
* @param loginPage the URL to send users to if login is required
* @return the {@link OAuth2LoginSpec} for further configuration
* @since 6.5
*/
public OneTimeTokenLoginSpec loginPage(String loginPage) {
Assert.hasText(loginPage, "loginPage cannot be empty");
this.loginPage = loginPage;
return this;
}
}
}

View File

@ -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.
@ -62,12 +62,12 @@ class OneTimeTokenLoginDsl {
tokenService?.also { oneTimeTokenLoginConfigurer.tokenService(tokenService) }
authenticationConverter?.also { oneTimeTokenLoginConfigurer.authenticationConverter(authenticationConverter) }
authenticationFailureHandler?.also {
oneTimeTokenLoginConfigurer.authenticationFailureHandler(
oneTimeTokenLoginConfigurer.failureHandler(
authenticationFailureHandler
)
}
authenticationSuccessHandler?.also {
oneTimeTokenLoginConfigurer.authenticationSuccessHandler(
oneTimeTokenLoginConfigurer.successHandler(
authenticationSuccessHandler
)
}

View File

@ -146,8 +146,8 @@ public class OneTimeTokenLoginConfigurerTests {
}
@Test
void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() throws Exception {
this.spring.register(OneTimeTokenFormLoginConfig.class).autowire();
void oneTimeTokenWhenConfiguredThenRendersRequestTokenForm() throws Exception {
this.spring.register(OneTimeTokenDefaultConfig.class).autowire();
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "BaseSpringSpec_CSRFTOKEN");
String csrfAttributeName = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
//@formatter:off
@ -168,21 +168,7 @@ public class OneTimeTokenLoginConfigurerTests {
</head>
<body>
<div class="content">
<form class="login-form" method="post" action="/login">
<h2>Please sign in</h2>
<p>
<label for="username" class="screenreader">Username</label>
<input type="text" id="username" name="username" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="screenreader">Password</label>
<input type="password" id="password" name="password" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="%s" />
<button type="submit" class="primary">Sign in</button>
</form>
<form id="ott-form" class="login-form" method="post" action="/ott/generate">
<h2>Request a One-Time Token</h2>
@ -202,6 +188,14 @@ public class OneTimeTokenLoginConfigurerTests {
//@formatter:on
}
@Test
void oneTimeTokenWhenLoginPageConfiguredThenRedirects() throws Exception {
this.spring.register(OneTimeTokenLoginPageConfig.class).autowire();
this.mvc.perform(get("/login"))
.andExpect(status().isFound())
.andExpect(redirectedUrl("http://localhost/custom-login"));
}
@Test
void oneTimeTokenWhenNoTokenGenerationSuccessHandlerThenException() {
assertThatException()
@ -304,6 +298,34 @@ public class OneTimeTokenLoginConfigurerTests {
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Import(UserDetailsServiceConfig.class)
static class OneTimeTokenLoginPageConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.oneTimeTokenLogin((ott) -> ott
.tokenGenerationSuccessHandler(ottSuccessHandler)
.loginPage("/custom-login")
);
// @formatter:on
return http.build();
}
@Bean
TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
return new TestOneTimeTokenGenerationSuccessHandler();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Import(UserDetailsServiceConfig.class)
@ -321,7 +343,7 @@ public class OneTimeTokenLoginConfigurerTests {
.tokenGeneratingUrl("/generateurl")
.tokenGenerationSuccessHandler(ottSuccessHandler)
.loginProcessingUrl("/loginprocessingurl")
.authenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/authenticated"))
.successHandler(new SimpleUrlAuthenticationSuccessHandler("/authenticated"))
);
// @formatter:on
return http.build();
@ -334,34 +356,6 @@ public class OneTimeTokenLoginConfigurerTests {
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Import(UserDetailsServiceConfig.class)
static class OneTimeTokenFormLoginConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGenerationSuccessHandler(ottSuccessHandler)
);
// @formatter:on
return http.build();
}
@Bean
TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
return new TestOneTimeTokenGenerationSuccessHandler();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Import(UserDetailsServiceConfig.class)

View File

@ -250,6 +250,41 @@ public class OneTimeTokenLoginSpecTests {
// @formatter:on
}
@Test
void oneTimeTokenWhenConfiguredThenRendersRequestTokenForm() {
this.spring.register(OneTimeTokenDefaultConfig.class).autowire();
//@formatter:off
byte[] responseByteArray = this.client.mutateWith(SecurityMockServerConfigurers.csrf())
.get()
.uri((uriBuilder) -> uriBuilder
.path("/login")
.build()
)
.exchange()
.expectBody()
.returnResult()
.getResponseBody();
// @formatter:on
String response = new String(responseByteArray);
assertThat(response.contains(EXPECTED_HTML_HEAD)).isTrue();
assertThat(response.contains(GENERATE_OTT_PART)).isTrue();
}
@Test
void oneTimeTokenWhenConfiguredThenRedirectsToLoginPage() {
this.spring.register(OneTimeTokenDefaultConfig.class).autowire();
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
.get()
.uri((uriBuilder) -> uriBuilder.path("/").build())
.exchange()
.expectHeader()
.location("/login");
}
@Test
void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() {
this.spring.register(OneTimeTokenFormLoginConfig.class).autowire();
@ -280,6 +315,18 @@ public class OneTimeTokenLoginSpecTests {
return lastToken;
}
@Test
void oneTimeTokenWhenCustomLoginPageThenRedirects() {
this.spring.register(OneTimeTokenDifferentUrlsConfig.class).autowire();
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
.get()
.uri((uriBuilder) -> uriBuilder.path("/login").build())
.exchange()
.expectHeader()
.location("/custom-login");
}
@Test
void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() {
assertThatException()
@ -362,6 +409,7 @@ public class OneTimeTokenLoginSpecTests {
.authenticated()
)
.oneTimeTokenLogin((ott) -> ott
.loginPage("/custom-login")
.tokenGeneratingUrl("/generateurl")
.tokenGenerationSuccessHandler(ottSuccessHandler)
.loginProcessingUrl("/loginprocessingurl")

View File

@ -42,8 +42,8 @@ In the following sections we will explore how to configure OTT Login for your ne
[[default-pages]]
== Default Login Page and Default One-Time Token Submit Page
The `oneTimeTokenLogin()` DSL can be used in conjunction with `formLogin()`, which will produce an additional One-Time Token Request Form in the xref:servlet/authentication/passwords/form.adoc[default generated login page].
It will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
When the `oneTimeTokenLogin()` DSL is used, by default the One-Time Token Login Page is auto-generated by the org.springframework.security.web.authentication.ui:DefaultLoginPageGeneratingFilter[].
The DSL will also set up the javadoc:org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter[] to generate a default One-Time Token submit page.
[[sending-token-to-user]]
== Sending the Token to the User

View File

@ -43,11 +43,13 @@ import static org.springframework.security.web.util.matcher.AntPathRequestMatche
*/
public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
public static final String DEFAULT_GENERATE_URL = "/ott/generate";
private final OneTimeTokenService tokenService;
private final OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler;
private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate");
private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, DEFAULT_GENERATE_URL);
private GenerateOneTimeTokenRequestResolver requestResolver = new DefaultGenerateOneTimeTokenRequestResolver();

View File

@ -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.
@ -133,7 +133,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
}
public boolean isEnabled() {
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled || this.oneTimeTokenEnabled;
}
public void setLogoutSuccessUrl(String logoutSuccessUrl) {

View File

@ -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.
@ -28,6 +28,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@ -43,11 +44,13 @@ import org.springframework.web.filter.OncePerRequestFilter;
*/
public final class DefaultOneTimeTokenSubmitPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/login/ott", "GET");
public static final String DEFAULT_SUBMIT_PAGE_URL = "/login/ott";
private RequestMatcher requestMatcher = new AntPathRequestMatcher(DEFAULT_SUBMIT_PAGE_URL, "GET");
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> Collections.emptyMap();
private String loginProcessingUrl = "/login/ott";
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)