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
Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
  • Loading branch information
Kehrlann committed Jan 24, 2025
1 parent 5d9011b commit 3541139
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 103 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 Expand Up @@ -31,6 +31,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
Expand Down Expand Up @@ -101,6 +102,7 @@ final class FilterOrderRegistration {
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
put(OneTimeTokenAuthenticationFilter.class, order.next());
order.next(); // gh-8105
put(DefaultResourcesFilter.class, order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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 @@ -33,51 +32,88 @@
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;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
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 = "/login/ott";

private String tokenGeneratingUrl = "/ott/generate";
private String tokenGeneratingUrl = GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL;

private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;

Expand All @@ -86,55 +122,51 @@ 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);
http.authenticationProvider(postProcess(authenticationProvider));
configureDefaultLoginPage(http);
intiDefaultLoginFilter(http);
}

private AuthenticationProvider getAuthenticationProvider(H http) {
if (this.authenticationProvider != null) {
return this.authenticationProvider;
}
UserDetailsService userDetailsService = getContext().getBean(UserDetailsService.class);
this.authenticationProvider = new OneTimeTokenAuthenticationProvider(getOneTimeTokenService(http),
userDetailsService);
return this.authenticationProvider;
}

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);
AuthenticationFilter oneTimeTokenAuthenticationFilter = new AuthenticationFilter(authenticationManager,
this.authenticationConverter);
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
oneTimeTokenAuthenticationFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler);
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 @@ -166,18 +198,13 @@ 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));
}

private AuthenticationProvider getAuthenticationProvider(H http) {
if (this.authenticationProvider != null) {
return this.authenticationProvider;
}
UserDetailsService userDetailsService = getContext().getBean(UserDetailsService.class);
this.authenticationProvider = new OneTimeTokenAuthenticationProvider(getOneTimeTokenService(http),
userDetailsService);
return this.authenticationProvider;
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return antMatcher(HttpMethod.POST, loginProcessingUrl);
}

/**
Expand Down Expand Up @@ -217,14 +244,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 @@ -269,7 +307,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 @@ -279,11 +317,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 @@ -292,22 +332,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
@@ -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 Expand Up @@ -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
)
}
Expand Down
Loading

0 comments on commit 3541139

Please sign in to comment.