diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java index 6f297cdb231..23f71504615 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java @@ -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. @@ -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; @@ -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()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java index cb34bf5f3fd..6f1e02ca6ed 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java @@ -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; @@ -32,12 +31,14 @@ 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; @@ -45,38 +46,75 @@ 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> - extends AbstractHttpConfigurer, H> { +/** + * An {@link AbstractHttpConfigurer} for One-Time Token Login. + * + *

+ * 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. + * + *

+ * 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. + * + *

Security Filters

+ * + * The following {@code Filter}s are populated: + * + * + * + *

Shared Objects Used

+ * + * The following shared objects are used: + * + * + * + * @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> extends + AbstractAuthenticationFilterConfigurer, 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 loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL; - private String tokenGeneratingUrl = "/ott/generate"; + private String tokenGeneratingUrl = GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL; private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler; @@ -85,55 +123,41 @@ public final class OneTimeTokenLoginConfigurer> 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); - 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) { @@ -167,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)); } @@ -181,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 @@ -218,14 +247,25 @@ public OneTimeTokenLoginConfigurer 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 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 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 @@ -270,7 +310,7 @@ public OneTimeTokenLoginConfigurer tokenService(OneTimeTokenService oneTimeTo */ public OneTimeTokenLoginConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - this.authenticationConverter = authenticationConverter; + this.getAuthenticationFilter().setAuthenticationConverter(authenticationConverter); return this; } @@ -280,11 +320,13 @@ public OneTimeTokenLoginConfigurer 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 authenticationFailureHandler( AuthenticationFailureHandler authenticationFailureHandler) { Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); - this.authenticationFailureHandler = authenticationFailureHandler; + super.failureHandler(authenticationFailureHandler); return this; } @@ -293,22 +335,16 @@ public OneTimeTokenLoginConfigurer 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 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, diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index ac72b75eb9a..12f7e5f1138 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -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; @@ -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); @@ -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) { @@ -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)); + } /** @@ -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; + } + } } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt index 2345bc5a679..8fa6d58fa5b 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDsl.kt @@ -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 ) } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java index a5f8417892f..0b965cfc2f8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java @@ -146,8 +146,8 @@ void oneTimeTokenWhenConfiguredThenServesCss() throws Exception { } @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 @@ void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() throws Exc
-