Skip to content

Commit 7f8b7bb

Browse files
Add support for One-Time Token Authentication
1 parent 161b0f3 commit 7f8b7bb

24 files changed

+1759
-1
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/HttpSecurityBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
3333
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3434
import org.springframework.security.web.authentication.logout.LogoutFilter;
35+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
3536
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
3637
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3738
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
@@ -150,6 +151,7 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
150151
* {@docRoot}/org/springframework/security/cas/web/CasAuthenticationFilter.html">CasAuthenticationFilter</a></li>
151152
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter}</li>
152153
* <li>{@link org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter}</li>
154+
* <li>{@link OneTimeTokenAuthenticationFilter}</li>
153155
* <li>{@link UsernamePasswordAuthenticationFilter}</li>
154156
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}</li>
155157
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter}</li>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
3030
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3131
import org.springframework.security.web.authentication.logout.LogoutFilter;
32+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
33+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationRequestFilter;
3234
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
3335
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3436
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
3537
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
3638
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
3739
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
40+
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
3841
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
3942
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
4043
import org.springframework.security.web.context.SecurityContextHolderFilter;
@@ -87,6 +90,7 @@ final class FilterOrderRegistration {
8790
this.filterToOrder.put(
8891
"org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter",
8992
order.next());
93+
put(OneTimeTokenAuthenticationRequestFilter.class, order.next());
9094
put(X509AuthenticationFilter.class, order.next());
9195
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
9296
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
@@ -95,10 +99,12 @@ final class FilterOrderRegistration {
9599
this.filterToOrder.put(
96100
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
97101
order.next());
102+
put(OneTimeTokenAuthenticationFilter.class, order.next());
98103
put(UsernamePasswordAuthenticationFilter.class, order.next());
99104
order.next(); // gh-8105
100105
put(DefaultLoginPageGeneratingFilter.class, order.next());
101106
put(DefaultLogoutPageGeneratingFilter.class, order.next());
107+
put(DefaultOneTimeTokenSubmitPageGeneratingFilter.class, order.next());
102108
put(ConcurrentSessionFilter.class, order.next());
103109
put(DigestAuthenticationFilter.class, order.next());
104110
this.filterToOrder.put(

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -72,6 +72,7 @@
7272
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
7373
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
7474
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
75+
import org.springframework.security.config.annotation.web.configurers.ott.OneTimeTokenLoginConfigurer;
7576
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
7677
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
7778
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2MetadataConfigurer;
@@ -2978,6 +2979,13 @@ public HttpSecurity oauth2ResourceServer(
29782979
return HttpSecurity.this;
29792980
}
29802981

2982+
public HttpSecurity oneTimeTokenLogin(
2983+
Customizer<OneTimeTokenLoginConfigurer<HttpSecurity>> oneTimeTokenLoginConfigurerCustomizer)
2984+
throws Exception {
2985+
oneTimeTokenLoginConfigurerCustomizer.customize(getOrApply(new OneTimeTokenLoginConfigurer<>(getContext())));
2986+
return HttpSecurity.this;
2987+
}
2988+
29812989
/**
29822990
* Configures channel security. In order for this configuration to be useful at least
29832991
* one mapping to a required channel must be provided.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.web.configurers.ott;
18+
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
import jakarta.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.http.HttpMethod;
27+
import org.springframework.security.authentication.AuthenticationManager;
28+
import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
29+
import org.springframework.security.authentication.ott.OneTimeToken;
30+
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
31+
import org.springframework.security.authentication.ott.OneTimeTokenSender;
32+
import org.springframework.security.authentication.ott.OneTimeTokenService;
33+
import org.springframework.security.config.Customizer;
34+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
35+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
36+
import org.springframework.security.core.Authentication;
37+
import org.springframework.security.core.userdetails.UserDetailsService;
38+
import org.springframework.security.web.authentication.AuthenticationConverter;
39+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
40+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
41+
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
42+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
43+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
44+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
45+
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationRequestFilter;
46+
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
47+
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
48+
import org.springframework.security.web.csrf.CsrfToken;
49+
import org.springframework.util.Assert;
50+
51+
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
52+
53+
public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
54+
extends AbstractHttpConfigurer<OneTimeTokenLoginConfigurer<H>, H> {
55+
56+
private final ApplicationContext context;
57+
58+
private OneTimeTokenService oneTimeTokenService;
59+
60+
private AuthenticationConverter authenticationConverter = new OneTimeTokenAuthenticationConverter();
61+
62+
private AuthenticationFailureHandler authenticationFailureHandler;
63+
64+
private AuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
65+
66+
private OneTimeTokenSender oneTimeTokenSender;
67+
68+
private String submitPageUrl = "/login/ott";
69+
70+
private boolean submitPageEnabled = true;
71+
72+
private String loginProcessingUrl = "/login/ott";
73+
74+
private String authenticationRequestUrl = "/ott/authenticate";
75+
76+
private String authenticationRequestRedirectUrl = "/login/ott";
77+
78+
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
79+
this.context = context;
80+
}
81+
82+
@Override
83+
public void init(H http) {
84+
UserDetailsService userDetailsService = getContext().getBean(UserDetailsService.class);
85+
OneTimeTokenAuthenticationProvider authenticationProvider = new OneTimeTokenAuthenticationProvider(
86+
getOneTimeTokenService(http), userDetailsService);
87+
http.authenticationProvider(postProcess(authenticationProvider));
88+
configureDefaultLoginPage(http);
89+
}
90+
91+
private void configureDefaultLoginPage(H http) {
92+
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
93+
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
94+
if (loginPageGeneratingFilter == null) {
95+
return;
96+
}
97+
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
98+
if (this.authenticationFailureHandler == null) {
99+
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler(loginPageGeneratingFilter.getLoginPageUrl() + "?error");
100+
}
101+
}
102+
103+
@Override
104+
public void configure(H http) {
105+
configureSubmitPage(http);
106+
configureOttAuthenticationRequestFilter(http);
107+
configureOttAuthenticationFilter(http);
108+
}
109+
110+
private void configureOttAuthenticationFilter(H http) {
111+
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
112+
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter(
113+
authenticationManager, this.authenticationConverter);
114+
oneTimeTokenAuthenticationFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
115+
oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
116+
oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler);
117+
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
118+
}
119+
120+
private void configureOttAuthenticationRequestFilter(H http) {
121+
OneTimeTokenAuthenticationRequestFilter authenticationRequestFilter = new OneTimeTokenAuthenticationRequestFilter(
122+
getOneTimeTokenService(http), getOneTimeTokenSender(http));
123+
authenticationRequestFilter.setRedirectUrl(this.authenticationRequestRedirectUrl);
124+
authenticationRequestFilter.setRequestMatcher(antMatcher(HttpMethod.GET, this.authenticationRequestUrl));
125+
http.addFilter(postProcess(authenticationRequestFilter));
126+
}
127+
128+
private void configureSubmitPage(H http) {
129+
if (!this.submitPageEnabled) {
130+
return;
131+
}
132+
DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter();
133+
submitPage.setResolveHiddenInputs(this::hiddenInputs);
134+
submitPage.setRequestMatcher(antMatcher(HttpMethod.GET, this.submitPageUrl));
135+
submitPage.setLoginProcessingUrl(this.loginProcessingUrl);
136+
http.addFilter(postProcess(submitPage));
137+
}
138+
139+
/**
140+
* Specifies the URL that a One-Time Token authentication request will be processed.
141+
* Defaults to {@code POST /ott/authenticate}.
142+
* @param authenticationRequestUrl
143+
*/
144+
public void authenticationRequestUrl(String authenticationRequestUrl) {
145+
Assert.hasText(authenticationRequestUrl, "authenticationRequestUrl cannot be null or empty");
146+
this.authenticationRequestUrl = authenticationRequestUrl;
147+
}
148+
149+
/**
150+
* Specifies the URL that the user-agent will be redirected after a successful One-Time Token authentication.
151+
* Defaults to {@code POST /login/ott}. If you are using the default submit page make sure that you also
152+
* configure {@link #submitPageUrl(String)} to this same URL.
153+
* @param authenticationRequestRedirectUrl
154+
*/
155+
public void authenticationRequestRedirectUrl(String authenticationRequestRedirectUrl) {
156+
Assert.hasText(authenticationRequestRedirectUrl, "authenticationRequestRedirectUrl cannot be null or empty");
157+
this.authenticationRequestRedirectUrl = authenticationRequestRedirectUrl;
158+
}
159+
160+
/**
161+
* Specifies the URL to process the login request, defaults to {@code /login/ott}. Only POST requests are processed, for that
162+
* reason make sure that you pass a valid CSRF token if CSRF protection is enabled.
163+
* @param loginProcessingUrl
164+
* @see org.springframework.security.config.annotation.web.builders.HttpSecurity#csrf(Customizer)
165+
*/
166+
public void loginProcessingUrl(String loginProcessingUrl) {
167+
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
168+
this.loginProcessingUrl = loginProcessingUrl;
169+
}
170+
171+
/**
172+
* Configures whether the default one-time token submit page should be shown.
173+
* This will prevent the {@link DefaultOneTimeTokenSubmitPageGeneratingFilter} to be configured.
174+
* @param show
175+
*/
176+
public void showSubmitPage(boolean show) {
177+
this.submitPageEnabled = show;
178+
}
179+
180+
/**
181+
* Sets the URL that the default submit page will be generated. Defaults to {@code /login/ott}.
182+
* Note that if you don't want to generate the default submit page you should use {@link #showSubmitPage(boolean)}.
183+
* @param submitPageUrl
184+
*/
185+
public void submitPageUrl(String submitPageUrl) {
186+
Assert.hasText(submitPageUrl, "submitPageUrl cannot be null or empty");
187+
this.submitPageUrl = submitPageUrl;
188+
}
189+
190+
/**
191+
* Specifies the {@link OneTimeTokenSender} to send the generated {@link OneTimeToken}
192+
* to the user
193+
* @param oneTimeTokenSender
194+
*/
195+
public void oneTimeTokenSender(OneTimeTokenSender oneTimeTokenSender) {
196+
Assert.notNull(oneTimeTokenSender, "oneTimeTokenSender cannot be null");
197+
this.oneTimeTokenSender = oneTimeTokenSender;
198+
}
199+
200+
/**
201+
* Configures the {@link OneTimeTokenService} used to generate and consume
202+
* {@link OneTimeToken}
203+
*
204+
* @param oneTimeTokenService
205+
*/
206+
public void oneTimeTokenService(OneTimeTokenService oneTimeTokenService) {
207+
Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null");
208+
this.oneTimeTokenService = oneTimeTokenService;
209+
}
210+
211+
/**
212+
* Use this {@link AuthenticationConverter} when converting incoming requests to an
213+
* {@link Authentication}. By default, the {@link OneTimeTokenAuthenticationConverter}
214+
* is used.
215+
*
216+
* @param authenticationConverter the {@link AuthenticationConverter} to use
217+
*/
218+
public void authenticationConverter(AuthenticationConverter authenticationConverter) {
219+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
220+
this.authenticationConverter = authenticationConverter;
221+
}
222+
223+
/**
224+
* Specifies the {@link AuthenticationFailureHandler} to use when authentication
225+
* fails. The default is redirecting to "/login?error" using
226+
* {@link SimpleUrlAuthenticationFailureHandler}
227+
*
228+
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
229+
* when authentication fails.
230+
*/
231+
public void authenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
232+
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
233+
this.authenticationFailureHandler = authenticationFailureHandler;
234+
}
235+
236+
/**
237+
* Specifies the {@link AuthenticationSuccessHandler} to be used. The default is
238+
* {@link SavedRequestAwareAuthenticationSuccessHandler} with no additional properties
239+
* set.
240+
*
241+
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler}.
242+
*/
243+
public void authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
244+
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
245+
this.authenticationSuccessHandler = authenticationSuccessHandler;
246+
}
247+
248+
private AuthenticationFailureHandler getAuthenticationFailureHandler() {
249+
if (this.authenticationFailureHandler != null) {
250+
return this.authenticationFailureHandler;
251+
}
252+
this.authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error");
253+
return this.authenticationFailureHandler;
254+
}
255+
256+
private OneTimeTokenService getOneTimeTokenService(H http) {
257+
if (this.oneTimeTokenService != null) {
258+
return this.oneTimeTokenService;
259+
}
260+
OneTimeTokenService bean = getBeanOrNull(http, OneTimeTokenService.class);
261+
if (bean != null) {
262+
this.oneTimeTokenService = bean;
263+
} else {
264+
this.oneTimeTokenService = new InMemoryOneTimeTokenService();
265+
}
266+
return this.oneTimeTokenService;
267+
}
268+
269+
private OneTimeTokenSender getOneTimeTokenSender(H http) {
270+
if (this.oneTimeTokenSender != null) {
271+
return this.oneTimeTokenSender;
272+
}
273+
OneTimeTokenSender bean = getBeanOrNull(http, OneTimeTokenSender.class);
274+
if (bean == null) {
275+
throw new IllegalStateException("A OneTimeTokenSender is required for oneTimeTokenLogin(). "
276+
+ "Please define a bean or pass an instance to the DSL.");
277+
}
278+
this.oneTimeTokenSender = bean;
279+
return this.oneTimeTokenSender;
280+
}
281+
282+
private <C> C getBeanOrNull(H http, Class<C> clazz) {
283+
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
284+
if (context == null) {
285+
return null;
286+
}
287+
try {
288+
return context.getBean(clazz);
289+
} catch (NoSuchBeanDefinitionException ex) {
290+
return null;
291+
}
292+
}
293+
294+
private Map<String, String> hiddenInputs(HttpServletRequest request) {
295+
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
296+
return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken())
297+
: Collections.emptyMap();
298+
}
299+
300+
public ApplicationContext getContext() {
301+
return this.context;
302+
}
303+
304+
}

0 commit comments

Comments
 (0)