Skip to content

Commit

Permalink
One Time Token login registers the default login page
Browse files Browse the repository at this point in the history
closes gh-16414

Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
  • Loading branch information
Kehrlann committed Feb 5, 2025
1 parent 2d72856 commit 95001c3
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 106 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

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;
Expand All @@ -32,6 +31,9 @@
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;
Expand All @@ -49,34 +51,70 @@
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;

Expand All @@ -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) {
Expand Down Expand Up @@ -170,7 +191,7 @@ private void configureSubmitPage(H http) {
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));
}

Expand All @@ -184,6 +205,11 @@ private AuthenticationProvider getAuthenticationProvider() {
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
Expand Down Expand Up @@ -221,14 +247,25 @@ public OneTimeTokenLoginConfigurer<H> tokenGenerationSuccessHandler(
* 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
Expand Down Expand Up @@ -273,7 +310,7 @@ public OneTimeTokenLoginConfigurer<H> tokenService(OneTimeTokenService oneTimeTo
*/
public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
this.getAuthenticationFilter().setAuthenticationConverter(authenticationConverter);
return this;
}

Expand All @@ -283,11 +320,13 @@ public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConv
* {@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;
}

Expand All @@ -296,22 +335,16 @@ public OneTimeTokenLoginConfigurer<H> authenticationFailureHandler(
* {@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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3035,7 +3035,8 @@ protected void configure(ServerHttpSecurity http) {
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;
Expand All @@ -3050,6 +3051,13 @@ protected void configure(ServerHttpSecurity http) {
}
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);
Expand Down Expand Up @@ -5948,11 +5956,13 @@ public final class OneTimeTokenLoginSpec {

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) {
Expand Down Expand Up @@ -5988,17 +5998,29 @@ private void configureOttGenerateFilter(ServerHttpSecurity http) {
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));

}

/**
Expand Down Expand Up @@ -6200,6 +6222,19 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
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;
}

}

}
Loading

0 comments on commit 95001c3

Please sign in to comment.