Skip to content

Commit c143c22

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

20 files changed

+1219
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
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.passwordless.PasswordlessAuthenticationFilter;
3233
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
3334
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3435
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
@@ -95,6 +96,7 @@ final class FilterOrderRegistration {
9596
this.filterToOrder.put(
9697
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
9798
order.next());
99+
put(PasswordlessAuthenticationFilter.class, order.next());
98100
put(UsernamePasswordAuthenticationFilter.class, order.next());
99101
order.next(); // gh-8105
100102
put(DefaultLoginPageGeneratingFilter.class, order.next());

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.passwordless.PasswordlessLoginConfigurer;
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 passwordlessLogin(
2983+
Customizer<PasswordlessLoginConfigurer<HttpSecurity>> passwordlessLoginConfigurerCustomizer)
2984+
throws Exception {
2985+
passwordlessLoginConfigurerCustomizer.customize(getOrApply(new PasswordlessLoginConfigurer<>(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,195 @@
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.passwordless;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import jakarta.servlet.http.HttpServletRequest;
25+
26+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.security.authentication.AuthenticationManager;
29+
import org.springframework.security.authentication.passwordless.ott.InMemoryOneTimeTokenService;
30+
import org.springframework.security.authentication.passwordless.ott.OneTimeTokenAuthenticationProvider;
31+
import org.springframework.security.authentication.passwordless.ott.OneTimeTokenService;
32+
import org.springframework.security.config.Customizer;
33+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
34+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
35+
import org.springframework.security.core.userdetails.UserDetailsService;
36+
import org.springframework.security.web.authentication.AuthenticationConverter;
37+
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
38+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
39+
import org.springframework.security.web.authentication.passwordless.PasswordlessAuthenticationFilter;
40+
import org.springframework.security.web.authentication.passwordless.ott.OneTimeTokenAuthenticationConverter;
41+
import org.springframework.security.web.authentication.passwordless.ott.OneTimeTokenAuthenticationRequestFilter;
42+
import org.springframework.security.web.authentication.passwordless.ott.OneTimeTokenAuthenticationRequestSuccessHandler;
43+
import org.springframework.security.web.authentication.passwordless.ott.RedirectOneTimeTokenAuthenticationRequestSuccessHandler;
44+
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
45+
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenConfirmationPageGeneratingFilter;
46+
import org.springframework.security.web.csrf.CsrfToken;
47+
import org.springframework.util.Assert;
48+
49+
public final class PasswordlessLoginConfigurer<H extends HttpSecurityBuilder<H>>
50+
extends AbstractHttpConfigurer<PasswordlessLoginConfigurer<H>, H> {
51+
52+
private final ApplicationContext context;
53+
54+
private final List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
55+
56+
private OneTimeTokenConfigurer oneTimeTokenConfigurer;
57+
58+
public PasswordlessLoginConfigurer(ApplicationContext context) {
59+
this.context = context;
60+
}
61+
62+
@Override
63+
public void init(H builder) throws Exception {
64+
if (this.oneTimeTokenConfigurer != null) {
65+
this.oneTimeTokenConfigurer.init(builder);
66+
}
67+
}
68+
69+
@Override
70+
public void configure(H http) throws Exception {
71+
if (this.oneTimeTokenConfigurer != null) {
72+
this.oneTimeTokenConfigurer.configure(http);
73+
}
74+
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
75+
DelegatingAuthenticationConverter authenticationConverter = getAuthenticationConverter();
76+
http.addFilter(new PasswordlessAuthenticationFilter(authenticationManager, authenticationConverter));
77+
}
78+
79+
public PasswordlessLoginConfigurer<H> oneTimeToken(
80+
Customizer<OneTimeTokenConfigurer> oneTimeTokenConfigurerCustomizer) {
81+
if (this.oneTimeTokenConfigurer == null) {
82+
this.oneTimeTokenConfigurer = new OneTimeTokenConfigurer();
83+
}
84+
oneTimeTokenConfigurerCustomizer.customize(this.oneTimeTokenConfigurer);
85+
return this;
86+
}
87+
88+
private DelegatingAuthenticationConverter getAuthenticationConverter() {
89+
if (this.authenticationConverters.isEmpty()) {
90+
throw new IllegalStateException(
91+
"No authentication converters configured for passwordless login. Please configure at least one passwordless login method");
92+
}
93+
return new DelegatingAuthenticationConverter(this.authenticationConverters);
94+
}
95+
96+
public ApplicationContext getContext() {
97+
return this.context;
98+
}
99+
100+
public final class OneTimeTokenConfigurer {
101+
102+
private OneTimeTokenAuthenticationRequestSuccessHandler successHandler = new RedirectOneTimeTokenAuthenticationRequestSuccessHandler(
103+
"/login/ott");
104+
105+
private OneTimeTokenService oneTimeTokenService;
106+
107+
private AuthenticationConverter authenticationConverter;
108+
109+
private void init(H http) {
110+
UserDetailsService userDetailsService = getContext().getBean(UserDetailsService.class);
111+
OneTimeTokenAuthenticationProvider authenticationProvider = new OneTimeTokenAuthenticationProvider(
112+
getOneTimeTokenService(http), userDetailsService);
113+
http.authenticationProvider(postProcess(authenticationProvider));
114+
PasswordlessLoginConfigurer.this.authenticationConverters.add(getAuthenticationConverter());
115+
initDefaultLoginPage(http);
116+
}
117+
118+
private void initDefaultLoginPage(H http) {
119+
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
120+
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
121+
if (loginPageGeneratingFilter != null) {
122+
loginPageGeneratingFilter.setOneTimeTokenEnabled(true);
123+
}
124+
}
125+
126+
private void configure(H http) {
127+
OneTimeTokenAuthenticationRequestFilter authenticationRequestFilter = new OneTimeTokenAuthenticationRequestFilter(
128+
getOneTimeTokenService(http));
129+
authenticationRequestFilter.setSuccessHandler(this.successHandler);
130+
http.addFilterBefore(postProcess(authenticationRequestFilter), UsernamePasswordAuthenticationFilter.class);
131+
132+
DefaultOneTimeTokenConfirmationPageGeneratingFilter confirmationPage = new DefaultOneTimeTokenConfirmationPageGeneratingFilter();
133+
confirmationPage.setResolveHiddenInputs(this::hiddenInputs);
134+
http.addFilterBefore(postProcess(confirmationPage), DefaultLoginPageGeneratingFilter.class);
135+
}
136+
137+
public void authenticationRequestSuccessHandler(
138+
OneTimeTokenAuthenticationRequestSuccessHandler authenticationRequestSuccessHandler) {
139+
Assert.notNull(authenticationRequestSuccessHandler, "authenticationRequestSuccessHandler cannot be null");
140+
this.successHandler = authenticationRequestSuccessHandler;
141+
}
142+
143+
public void oneTimeTokenService(OneTimeTokenService oneTimeTokenService) {
144+
Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null");
145+
this.oneTimeTokenService = oneTimeTokenService;
146+
}
147+
148+
public void authenticationConverter(AuthenticationConverter authenticationConverter) {
149+
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
150+
this.authenticationConverter = authenticationConverter;
151+
}
152+
153+
private AuthenticationConverter getAuthenticationConverter() {
154+
if (this.authenticationConverter == null) {
155+
this.authenticationConverter = new OneTimeTokenAuthenticationConverter();
156+
}
157+
return this.authenticationConverter;
158+
}
159+
160+
private OneTimeTokenService getOneTimeTokenService(H http) {
161+
if (this.oneTimeTokenService != null) {
162+
return this.oneTimeTokenService;
163+
}
164+
OneTimeTokenService bean = getBeanOrNull(http, OneTimeTokenService.class);
165+
if (bean != null) {
166+
this.oneTimeTokenService = bean;
167+
}
168+
else {
169+
this.oneTimeTokenService = new InMemoryOneTimeTokenService();
170+
}
171+
return this.oneTimeTokenService;
172+
}
173+
174+
private <C> C getBeanOrNull(H http, Class<C> clazz) {
175+
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
176+
if (context == null) {
177+
return null;
178+
}
179+
try {
180+
return context.getBean(clazz);
181+
}
182+
catch (NoSuchBeanDefinitionException ex) {
183+
return null;
184+
}
185+
}
186+
187+
private Map<String, String> hiddenInputs(HttpServletRequest request) {
188+
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
189+
return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken())
190+
: Collections.emptyMap();
191+
}
192+
193+
}
194+
195+
}

0 commit comments

Comments
 (0)