OneTimeTokenLoginConfigurer.java
/*
* Copyright 2004-present 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.config.annotation.web.configurers.ott;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
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.OneTimeToken;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
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.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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.csrf.CsrfToken;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* 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 String defaultSubmitPageUrl = DefaultOneTimeTokenSubmitPageGeneratingFilter.DEFAULT_SUBMIT_PAGE_URL;
private boolean submitPageEnabled = true;
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
private String tokenGeneratingUrl = GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL;
private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
private AuthenticationProvider authenticationProvider;
private GenerateOneTimeTokenRequestResolver requestResolver;
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
super(new OneTimeTokenAuthenticationFilter(), null);
this.context = context;
}
@Override
public void init(H http) {
if (getLoginProcessingUrl() == null) {
loginProcessingUrl(OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL);
}
super.init(http);
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
http.authenticationProvider(postProcess(authenticationProvider));
intiDefaultLoginFilter(http);
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptions != null) {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
exceptions.defaultDeniedHandlerForMissingAuthority((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
FactorGrantedAuthority.OTT_AUTHORITY);
}
}
private void intiDefaultLoginFilter(H http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter == null || isCustomLoginPage()) {
return;
}
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
loginPageGeneratingFilter.setOneTimeTokenGenerationUrl(this.tokenGeneratingUrl);
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) {
super.configure(http);
configureSubmitPage(http);
configureOttGenerateFilter(http);
}
private void configureOttGenerateFilter(H http) {
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(),
getOneTimeTokenGenerationSuccessHandler());
generateFilter.setRequestMatcher(getRequestMatcherBuilder().matcher(HttpMethod.POST, this.tokenGeneratingUrl));
generateFilter.setRequestResolver(getGenerateRequestResolver());
http.addFilter(postProcess(generateFilter));
http.addFilter(DefaultResourcesFilter.css());
}
private OneTimeTokenGenerationSuccessHandler getOneTimeTokenGenerationSuccessHandler() {
if (this.oneTimeTokenGenerationSuccessHandler == null) {
this.oneTimeTokenGenerationSuccessHandler = this.context
.getBeanProvider(OneTimeTokenGenerationSuccessHandler.class)
.getIfUnique();
}
if (this.oneTimeTokenGenerationSuccessHandler == null) {
throw new IllegalStateException("""
A OneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
""");
}
return this.oneTimeTokenGenerationSuccessHandler;
}
private void configureSubmitPage(H http) {
if (!this.submitPageEnabled) {
return;
}
DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter();
submitPage.setResolveHiddenInputs(this::hiddenInputs);
submitPage.setRequestMatcher(getRequestMatcherBuilder().matcher(HttpMethod.GET, this.defaultSubmitPageUrl));
submitPage.setLoginProcessingUrl(this.getLoginProcessingUrl());
http.addFilter(postProcess(submitPage));
}
private AuthenticationProvider getAuthenticationProvider() {
if (this.authenticationProvider != null) {
return this.authenticationProvider;
}
UserDetailsService userDetailsService = this.context.getBean(UserDetailsService.class);
this.authenticationProvider = new OneTimeTokenAuthenticationProvider(getOneTimeTokenService(),
userDetailsService);
return this.authenticationProvider;
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return getRequestMatcherBuilder().matcher(HttpMethod.POST, loginProcessingUrl);
}
/**
* Specifies the {@link AuthenticationProvider} to use when authenticating the user.
* @param authenticationProvider
*/
public OneTimeTokenLoginConfigurer<H> authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProvider = authenticationProvider;
return this;
}
/**
* Specifies the URL that a One-Time Token generate request will be processed.
* Defaults to {@code /ott/generate}.
* @param tokenGeneratingUrl
*/
public OneTimeTokenLoginConfigurer<H> tokenGeneratingUrl(String tokenGeneratingUrl) {
Assert.hasText(tokenGeneratingUrl, "tokenGeneratingUrl cannot be null or empty");
this.tokenGeneratingUrl = tokenGeneratingUrl;
return this;
}
/**
* Specifies strategy to be used to handle generated one-time tokens.
* @param oneTimeTokenGenerationSuccessHandler
*/
public OneTimeTokenLoginConfigurer<H> tokenGenerationSuccessHandler(
OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) {
Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null");
this.oneTimeTokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler;
return this;
}
/**
* Specifies the URL to process the login request, defaults to {@code /login/ott}.
* 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 HttpSecurity#csrf(Customizer)
*/
public OneTimeTokenLoginConfigurer<H> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
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
* configured.
* @param show
*/
public OneTimeTokenLoginConfigurer<H> showDefaultSubmitPage(boolean show) {
this.submitPageEnabled = show;
return this;
}
/**
* Sets the URL that the default submit page will be generated. Defaults to
* {@code /login/ott}. If you don't want to generate the default submit page you
* should use {@link #showDefaultSubmitPage(boolean)}. Note that this method always
* invoke {@link #showDefaultSubmitPage(boolean)} passing {@code true}.
* @param submitPageUrl
*/
public OneTimeTokenLoginConfigurer<H> defaultSubmitPageUrl(String submitPageUrl) {
Assert.hasText(submitPageUrl, "submitPageUrl cannot be null or empty");
this.defaultSubmitPageUrl = submitPageUrl;
showDefaultSubmitPage(true);
return this;
}
/**
* Configures the {@link OneTimeTokenService} used to generate and consume
* {@link OneTimeToken}
* @param oneTimeTokenService
*/
public OneTimeTokenLoginConfigurer<H> tokenService(OneTimeTokenService oneTimeTokenService) {
Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null");
this.oneTimeTokenService = oneTimeTokenService;
return this;
}
/**
* Use this {@link AuthenticationConverter} when converting incoming requests to an
* {@link Authentication}. By default, the {@link OneTimeTokenAuthenticationConverter}
* is used.
* @param authenticationConverter the {@link AuthenticationConverter} to use
*/
public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.getAuthenticationFilter().setAuthenticationConverter(authenticationConverter);
return this;
}
/**
* Specifies the {@link AuthenticationFailureHandler} to use when authentication
* fails. The default is redirecting to "/login?error" using
* {@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");
super.failureHandler(authenticationFailureHandler);
return this;
}
/**
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
* {@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");
super.successHandler(authenticationSuccessHandler);
return this;
}
/**
* 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() {
if (this.requestResolver != null) {
return this.requestResolver;
}
this.requestResolver = this.context.getBeanProvider(GenerateOneTimeTokenRequestResolver.class)
.getIfUnique(DefaultGenerateOneTimeTokenRequestResolver::new);
return this.requestResolver;
}
private OneTimeTokenService getOneTimeTokenService() {
if (this.oneTimeTokenService != null) {
return this.oneTimeTokenService;
}
this.oneTimeTokenService = this.context.getBeanProvider(OneTimeTokenService.class)
.getIfUnique(InMemoryOneTimeTokenService::new);
return this.oneTimeTokenService;
}
private Map<String, String> hiddenInputs(HttpServletRequest request) {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken())
: Collections.emptyMap();
}
/**
* @deprecated Use this.context instead
*/
@Deprecated
public ApplicationContext getContext() {
return this.context;
}
}