Skip to content

Commit 95001c3

Browse files
committed
One Time Token login registers the default login page
closes gh-16414 Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
1 parent 2d72856 commit 95001c3

File tree

10 files changed

+221
-106
lines changed

10 files changed

+221
-106
lines changed

Diff for: config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

Diff for: config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java

+92-59
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import org.springframework.context.ApplicationContext;
2525
import org.springframework.http.HttpMethod;
26-
import org.springframework.security.authentication.AuthenticationManager;
2726
import org.springframework.security.authentication.AuthenticationProvider;
2827
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
2928
import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
@@ -32,6 +31,9 @@
3231
import org.springframework.security.authentication.ott.OneTimeTokenService;
3332
import org.springframework.security.config.Customizer;
3433
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
34+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
35+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
36+
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
3537
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
3638
import org.springframework.security.core.Authentication;
3739
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -49,34 +51,70 @@
4951
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
5052
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
5153
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
52-
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
53-
import org.springframework.security.web.context.SecurityContextRepository;
5454
import org.springframework.security.web.csrf.CsrfToken;
55+
import org.springframework.security.web.util.matcher.RequestMatcher;
5556
import org.springframework.util.Assert;
5657
import org.springframework.util.StringUtils;
5758

5859
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
5960

60-
public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
61-
extends AbstractHttpConfigurer<OneTimeTokenLoginConfigurer<H>, H> {
61+
/**
62+
* An {@link AbstractHttpConfigurer} for One-Time Token Login.
63+
*
64+
* <p>
65+
* One-Time Token Login provides an application with the capability to have users log in
66+
* by obtaining a single-use token out of band, for example through email.
67+
*
68+
* <p>
69+
* Defaults are provided for all configuration options, with the only required
70+
* configuration being
71+
* {@link #tokenGenerationSuccessHandler(OneTimeTokenGenerationSuccessHandler)}.
72+
* Alternatively, a {@link OneTimeTokenGenerationSuccessHandler} {@code @Bean} may be
73+
* registered instead.
74+
*
75+
* <h2>Security Filters</h2>
76+
*
77+
* The following {@code Filter}s are populated:
78+
*
79+
* <ul>
80+
* <li>{@link DefaultOneTimeTokenSubmitPageGeneratingFilter}</li>
81+
* <li>{@link GenerateOneTimeTokenFilter}</li>
82+
* <li>{@link OneTimeTokenAuthenticationFilter}</li>
83+
* </ul>
84+
*
85+
* <h2>Shared Objects Used</h2>
86+
*
87+
* The following shared objects are used:
88+
*
89+
* <ul>
90+
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
91+
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
92+
* login page will be made available</li>
93+
* </ul>
94+
*
95+
* @author Marcus Da Coregio
96+
* @author Daniel Garnier-Moiroux
97+
* @since 6.4
98+
* @see HttpSecurity#oneTimeTokenLogin(Customizer)
99+
* @see DefaultOneTimeTokenSubmitPageGeneratingFilter
100+
* @see GenerateOneTimeTokenFilter
101+
* @see OneTimeTokenAuthenticationFilter
102+
* @see AbstractAuthenticationFilterConfigurer
103+
*/
104+
public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
105+
AbstractAuthenticationFilterConfigurer<H, OneTimeTokenLoginConfigurer<H>, OneTimeTokenAuthenticationFilter> {
62106

63107
private final ApplicationContext context;
64108

65109
private OneTimeTokenService oneTimeTokenService;
66110

67-
private AuthenticationConverter authenticationConverter = new OneTimeTokenAuthenticationConverter();
68-
69-
private AuthenticationFailureHandler authenticationFailureHandler;
70-
71-
private AuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
72-
73-
private String defaultSubmitPageUrl = "/login/ott";
111+
private String defaultSubmitPageUrl = DefaultOneTimeTokenSubmitPageGeneratingFilter.DEFAULT_SUBMIT_PAGE_URL;
74112

75113
private boolean submitPageEnabled = true;
76114

77115
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
78116

79-
private String tokenGeneratingUrl = "/ott/generate";
117+
private String tokenGeneratingUrl = GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL;
80118

81119
private OneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
82120

@@ -85,58 +123,41 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
85123
private GenerateOneTimeTokenRequestResolver requestResolver;
86124

87125
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
126+
super(new OneTimeTokenAuthenticationFilter(), OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL);
88127
this.context = context;
89128
}
90129

91130
@Override
92-
public void init(H http) {
131+
public void init(H http) throws Exception {
132+
super.init(http);
93133
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
94134
http.authenticationProvider(postProcess(authenticationProvider));
95-
configureDefaultLoginPage(http);
135+
intiDefaultLoginFilter(http);
96136
}
97137

98-
private void configureDefaultLoginPage(H http) {
138+
private void intiDefaultLoginFilter(H http) {
99139
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
100140
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
101-
if (loginPageGeneratingFilter == null) {
141+
if (loginPageGeneratingFilter == null || isCustomLoginPage()) {
102142
return;
103143
}
104144
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
105145
loginPageGeneratingFilter.setOneTimeTokenGenerationUrl(this.tokenGeneratingUrl);
106-
if (this.authenticationFailureHandler == null
107-
&& StringUtils.hasText(loginPageGeneratingFilter.getLoginPageUrl())) {
108-
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler(
109-
loginPageGeneratingFilter.getLoginPageUrl() + "?error");
146+
147+
if (!StringUtils.hasText(loginPageGeneratingFilter.getLoginPageUrl())) {
148+
loginPageGeneratingFilter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
149+
loginPageGeneratingFilter.setFailureUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL + "?"
150+
+ DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME);
151+
loginPageGeneratingFilter
152+
.setLogoutSuccessUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL + "?logout");
110153
}
111154
}
112155

113156
@Override
114-
public void configure(H http) {
157+
public void configure(H http) throws Exception {
158+
super.configure(http);
115159
configureSubmitPage(http);
116160
configureOttGenerateFilter(http);
117-
configureOttAuthenticationFilter(http);
118-
}
119-
120-
private void configureOttAuthenticationFilter(H http) {
121-
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
122-
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter();
123-
oneTimeTokenAuthenticationFilter.setAuthenticationManager(authenticationManager);
124-
if (this.loginProcessingUrl != null) {
125-
oneTimeTokenAuthenticationFilter
126-
.setRequiresAuthenticationRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
127-
}
128-
oneTimeTokenAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
129-
oneTimeTokenAuthenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
130-
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
131-
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
132-
}
133-
134-
private SecurityContextRepository getSecurityContextRepository(H http) {
135-
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
136-
if (securityContextRepository != null) {
137-
return securityContextRepository;
138-
}
139-
return new HttpSessionSecurityContextRepository();
140161
}
141162

142163
private void configureOttGenerateFilter(H http) {
@@ -170,7 +191,7 @@ private void configureSubmitPage(H http) {
170191
DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter();
171192
submitPage.setResolveHiddenInputs(this::hiddenInputs);
172193
submitPage.setRequestMatcher(antMatcher(HttpMethod.GET, this.defaultSubmitPageUrl));
173-
submitPage.setLoginProcessingUrl(this.loginProcessingUrl);
194+
submitPage.setLoginProcessingUrl(this.getLoginProcessingUrl());
174195
http.addFilter(postProcess(submitPage));
175196
}
176197

@@ -184,6 +205,11 @@ private AuthenticationProvider getAuthenticationProvider() {
184205
return this.authenticationProvider;
185206
}
186207

208+
@Override
209+
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
210+
return antMatcher(HttpMethod.POST, loginProcessingUrl);
211+
}
212+
187213
/**
188214
* Specifies the {@link AuthenticationProvider} to use when authenticating the user.
189215
* @param authenticationProvider
@@ -221,14 +247,25 @@ public OneTimeTokenLoginConfigurer<H> tokenGenerationSuccessHandler(
221247
* Only POST requests are processed, for that reason make sure that you pass a valid
222248
* CSRF token if CSRF protection is enabled.
223249
* @param loginProcessingUrl
224-
* @see org.springframework.security.config.annotation.web.builders.HttpSecurity#csrf(Customizer)
250+
* @see HttpSecurity#csrf(Customizer)
225251
*/
226252
public OneTimeTokenLoginConfigurer<H> loginProcessingUrl(String loginProcessingUrl) {
227253
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
228-
this.loginProcessingUrl = loginProcessingUrl;
254+
super.loginProcessingUrl(loginProcessingUrl);
229255
return this;
230256
}
231257

258+
/**
259+
* Specifies the URL to send users to if login is required. If used with
260+
* {@link EnableWebSecurity} a default login page will be generated when this
261+
* attribute is not specified.
262+
* @param loginPage
263+
*/
264+
@Override
265+
public OneTimeTokenLoginConfigurer<H> loginPage(String loginPage) {
266+
return super.loginPage(loginPage);
267+
}
268+
232269
/**
233270
* Configures whether the default one-time token submit page should be shown. This
234271
* will prevent the {@link DefaultOneTimeTokenSubmitPageGeneratingFilter} to be
@@ -273,7 +310,7 @@ public OneTimeTokenLoginConfigurer<H> tokenService(OneTimeTokenService oneTimeTo
273310
*/
274311
public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
275312
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
276-
this.authenticationConverter = authenticationConverter;
313+
this.getAuthenticationFilter().setAuthenticationConverter(authenticationConverter);
277314
return this;
278315
}
279316

@@ -283,11 +320,13 @@ public OneTimeTokenLoginConfigurer<H> authenticationConverter(AuthenticationConv
283320
* {@link SimpleUrlAuthenticationFailureHandler}
284321
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
285322
* when authentication fails.
323+
* @deprecated Use {@link #failureHandler(AuthenticationFailureHandler)} instead
286324
*/
325+
@Deprecated(since = "6.5")
287326
public OneTimeTokenLoginConfigurer<H> authenticationFailureHandler(
288327
AuthenticationFailureHandler authenticationFailureHandler) {
289328
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
290-
this.authenticationFailureHandler = authenticationFailureHandler;
329+
super.failureHandler(authenticationFailureHandler);
291330
return this;
292331
}
293332

@@ -296,22 +335,16 @@ public OneTimeTokenLoginConfigurer<H> authenticationFailureHandler(
296335
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
297336
* set.
298337
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler}.
338+
* @deprecated Use {@link #successHandler(AuthenticationSuccessHandler)} instead
299339
*/
340+
@Deprecated(since = "6.5")
300341
public OneTimeTokenLoginConfigurer<H> authenticationSuccessHandler(
301342
AuthenticationSuccessHandler authenticationSuccessHandler) {
302343
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
303-
this.authenticationSuccessHandler = authenticationSuccessHandler;
344+
super.successHandler(authenticationSuccessHandler);
304345
return this;
305346
}
306347

307-
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
308-
if (this.authenticationFailureHandler != null) {
309-
return this.authenticationFailureHandler;
310-
}
311-
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");
312-
return this.authenticationFailureHandler;
313-
}
314-
315348
/**
316349
* Use this {@link GenerateOneTimeTokenRequestResolver} when resolving
317350
* {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}. By default,

Diff for: config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+46-11
Original file line numberDiff line numberDiff line change
@@ -3035,7 +3035,8 @@ protected void configure(ServerHttpSecurity http) {
30353035
return;
30363036
}
30373037
if (http.formLogin != null && http.formLogin.isEntryPointExplicit
3038-
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)) {
3038+
|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)
3039+
|| http.oneTimeTokenLogin != null && StringUtils.hasText(http.oneTimeTokenLogin.loginPage)) {
30393040
return;
30403041
}
30413042
LoginPageGeneratingWebFilter loginPage = null;
@@ -3050,6 +3051,13 @@ protected void configure(ServerHttpSecurity http) {
30503051
}
30513052
loginPage.setOauth2AuthenticationUrlToClientName(urlToText);
30523053
}
3054+
if (http.oneTimeTokenLogin != null) {
3055+
if (loginPage == null) {
3056+
loginPage = new LoginPageGeneratingWebFilter();
3057+
}
3058+
loginPage.setOneTimeTokenEnabled(true);
3059+
loginPage.setGenerateOneTimeTokenUrl(http.oneTimeTokenLogin.tokenGeneratingUrl);
3060+
}
30533061
if (loginPage != null) {
30543062
http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
30553063
http.addFilterBefore(DefaultResourcesWebFilter.css(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
@@ -5948,11 +5956,13 @@ public final class OneTimeTokenLoginSpec {
59485956

59495957
private boolean submitPageEnabled = true;
59505958

5959+
private String loginPage;
5960+
59515961
protected void configure(ServerHttpSecurity http) {
59525962
configureSubmitPage(http);
59535963
configureOttGenerateFilter(http);
59545964
configureOttAuthenticationFilter(http);
5955-
configureDefaultLoginPage(http);
5965+
configureDefaultEntryPoint(http);
59565966
}
59575967

59585968
private void configureOttAuthenticationFilter(ServerHttpSecurity http) {
@@ -5988,17 +5998,29 @@ private void configureOttGenerateFilter(ServerHttpSecurity http) {
59885998
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
59895999
}
59906000

5991-
private void configureDefaultLoginPage(ServerHttpSecurity http) {
5992-
if (http.formLogin != null) {
5993-
for (WebFilter webFilter : http.webFilters) {
5994-
OrderedWebFilter orderedWebFilter = (OrderedWebFilter) webFilter;
5995-
if (orderedWebFilter.webFilter instanceof LoginPageGeneratingWebFilter loginPageGeneratingFilter) {
5996-
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
5997-
loginPageGeneratingFilter.setGenerateOneTimeTokenUrl(this.tokenGeneratingUrl);
5998-
break;
5999-
}
6001+
private void configureDefaultEntryPoint(ServerHttpSecurity http) {
6002+
MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
6003+
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
6004+
MediaType.TEXT_PLAIN);
6005+
htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
6006+
ServerWebExchangeMatcher xhrMatcher = (exchange) -> {
6007+
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
6008+
return ServerWebExchangeMatcher.MatchResult.match();
60006009
}
6010+
return ServerWebExchangeMatcher.MatchResult.notMatch();
6011+
};
6012+
ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
6013+
ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
6014+
htmlMatcher);
6015+
String loginPage = "/login";
6016+
if (this.loginPage != null) {
6017+
loginPage = this.loginPage;
60016018
}
6019+
RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
6020+
loginPage);
6021+
defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
6022+
http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
6023+
60026024
}
60036025

60046026
/**
@@ -6200,6 +6222,19 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
62006222
return this.tokenGenerationSuccessHandler;
62016223
}
62026224

6225+
/**
6226+
* Specifies the URL to send users to if login is required. A default login page
6227+
* will be generated when this attribute is not specified.
6228+
* @param loginPage the URL to send users to if login is required
6229+
* @return the {@link OAuth2LoginSpec} for further configuration
6230+
* @since 6.5
6231+
*/
6232+
public OneTimeTokenLoginSpec loginPage(String loginPage) {
6233+
Assert.hasText(loginPage, "loginPage cannot be empty");
6234+
this.loginPage = loginPage;
6235+
return this;
6236+
}
6237+
62036238
}
62046239

62056240
}

0 commit comments

Comments
 (0)