Skip to content

Commit 6d330d7

Browse files
Add Support GenerateOneTimeTokenRequestResolver
Closes gh-16291
1 parent 036f6f2 commit 6d330d7

File tree

9 files changed

+286
-9
lines changed

9 files changed

+286
-9
lines changed

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

+29
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
import java.util.Collections;
2020
import java.util.Map;
21+
import java.util.Objects;
2122

2223
import jakarta.servlet.http.HttpServletRequest;
2324

2425
import org.springframework.context.ApplicationContext;
2526
import org.springframework.http.HttpMethod;
2627
import org.springframework.security.authentication.AuthenticationManager;
2728
import org.springframework.security.authentication.AuthenticationProvider;
29+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
2830
import org.springframework.security.authentication.ott.InMemoryOneTimeTokenService;
2931
import org.springframework.security.authentication.ott.OneTimeToken;
3032
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationProvider;
@@ -40,7 +42,9 @@
4042
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
4143
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
4244
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
45+
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
4346
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
47+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
4448
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
4549
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
4650
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
@@ -79,6 +83,8 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
7983

8084
private AuthenticationProvider authenticationProvider;
8185

86+
private GenerateOneTimeTokenRequestResolver requestResolver;
87+
8288
public OneTimeTokenLoginConfigurer(ApplicationContext context) {
8389
this.context = context;
8490
}
@@ -135,6 +141,7 @@ private void configureOttGenerateFilter(H http) {
135141
GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(http),
136142
getOneTimeTokenGenerationSuccessHandler(http));
137143
generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl));
144+
generateFilter.setRequestResolver(getGenerateRequestResolver(http));
138145
http.addFilter(postProcess(generateFilter));
139146
http.addFilter(DefaultResourcesFilter.css());
140147
}
@@ -301,6 +308,28 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
301308
return this.authenticationFailureHandler;
302309
}
303310

311+
/**
312+
* Use this {@link GenerateOneTimeTokenRequestResolver} when resolving
313+
* {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}. By default,
314+
* the {@link DefaultGenerateOneTimeTokenRequestResolver} is used.
315+
* @param requestResolver the {@link GenerateOneTimeTokenRequestResolver}
316+
* @since 6.5
317+
*/
318+
public OneTimeTokenLoginConfigurer<H> generateRequestResolver(GenerateOneTimeTokenRequestResolver requestResolver) {
319+
Assert.notNull(requestResolver, "requestResolver cannot be null");
320+
this.requestResolver = requestResolver;
321+
return this;
322+
}
323+
324+
private GenerateOneTimeTokenRequestResolver getGenerateRequestResolver(H http) {
325+
if (this.requestResolver != null) {
326+
return this.requestResolver;
327+
}
328+
GenerateOneTimeTokenRequestResolver bean = getBeanOrNull(http, GenerateOneTimeTokenRequestResolver.class);
329+
this.requestResolver = Objects.requireNonNullElseGet(bean, DefaultGenerateOneTimeTokenRequestResolver::new);
330+
return this.requestResolver;
331+
}
332+
304333
private OneTimeTokenService getOneTimeTokenService(H http) {
305334
if (this.oneTimeTokenService != null) {
306335
return this.oneTimeTokenService;

config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java

+54
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.security.config.annotation.web.configurers.ott;
1818

1919
import java.io.IOException;
20+
import java.time.Instant;
21+
import java.time.ZoneOffset;
2022

2123
import jakarta.servlet.ServletException;
2224
import jakarta.servlet.http.HttpServletRequest;
@@ -29,6 +31,7 @@
2931
import org.springframework.context.annotation.Bean;
3032
import org.springframework.context.annotation.Configuration;
3133
import org.springframework.context.annotation.Import;
34+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
3235
import org.springframework.security.authentication.ott.OneTimeToken;
3336
import org.springframework.security.config.Customizer;
3437
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -40,6 +43,8 @@
4043
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
4144
import org.springframework.security.web.SecurityFilterChain;
4245
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
46+
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver;
47+
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
4348
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
4449
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
4550
import org.springframework.security.web.csrf.CsrfToken;
@@ -194,6 +199,55 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
194199
""");
195200
}
196201

202+
@Test
203+
void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception {
204+
this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire();
205+
this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf()))
206+
.andExpectAll(status().isFound(), redirectedUrl("/login/ott"));
207+
208+
OneTimeToken token = TestOneTimeTokenGenerationSuccessHandler.lastToken;
209+
210+
this.mvc.perform(post("/login/ott").param("token", token.getTokenValue()).with(csrf()))
211+
.andExpectAll(status().isFound(), redirectedUrl("/"), authenticated());
212+
assertThat(getCurrentMinutes(token.getExpiresAt())).isEqualTo(10);
213+
}
214+
215+
private int getCurrentMinutes(Instant expiresAt) {
216+
int expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).getMinute();
217+
int currentMinutes = Instant.now().atZone(ZoneOffset.UTC).getMinute();
218+
return expiresMinutes - currentMinutes;
219+
}
220+
221+
@Configuration(proxyBeanMethods = false)
222+
@EnableWebSecurity
223+
@Import(UserDetailsServiceConfig.class)
224+
static class OneTimeTokenConfigWithCustomTokenExpirationTime {
225+
226+
@Bean
227+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
228+
// @formatter:off
229+
http
230+
.authorizeHttpRequests((authz) -> authz
231+
.anyRequest().authenticated()
232+
)
233+
.oneTimeTokenLogin((ott) -> ott
234+
.tokenGenerationSuccessHandler(new TestOneTimeTokenGenerationSuccessHandler())
235+
);
236+
// @formatter:on
237+
return http.build();
238+
}
239+
240+
@Bean
241+
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
242+
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
243+
return (request) -> {
244+
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
245+
return new GenerateOneTimeTokenRequest(generate.getUsername(), 600);
246+
};
247+
}
248+
249+
}
250+
197251
@Configuration(proxyBeanMethods = false)
198252
@EnableWebSecurity
199253
@Import(UserDetailsServiceConfig.class)

core/src/main/java/org/springframework/security/authentication/ott/GenerateOneTimeTokenRequest.java

+22
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,37 @@
2626
*/
2727
public class GenerateOneTimeTokenRequest {
2828

29+
private static final int DEFAULT_EXPIRES_IN = 300;
30+
2931
private final String username;
3032

33+
private final int expiresIn;
34+
3135
public GenerateOneTimeTokenRequest(String username) {
3236
Assert.hasText(username, "username cannot be empty");
3337
this.username = username;
38+
this.expiresIn = DEFAULT_EXPIRES_IN;
39+
}
40+
41+
/**
42+
* Constructs an <code>GenerateOneTimeTokenRequest</code> with the specified username
43+
* and expiresIn
44+
* @param username username
45+
* @param expiresIn one-time token expiration time (seconds)
46+
*/
47+
public GenerateOneTimeTokenRequest(String username, int expiresIn) {
48+
Assert.hasText(username, "username cannot be empty");
49+
Assert.isTrue(expiresIn > 0, "expiresIn must be > 0");
50+
this.username = username;
51+
this.expiresIn = expiresIn;
3452
}
3553

3654
public String getUsername() {
3755
return this.username;
3856
}
3957

58+
public int getExpiresIn() {
59+
return this.expiresIn;
60+
}
61+
4062
}

core/src/main/java/org/springframework/security/authentication/ott/InMemoryOneTimeTokenService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public final class InMemoryOneTimeTokenService implements OneTimeTokenService {
4444
@NonNull
4545
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
4646
String token = UUID.randomUUID().toString();
47-
Instant fiveMinutesFromNow = this.clock.instant().plusSeconds(300);
48-
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
47+
Instant expiresAt = this.clock.instant().plusSeconds(request.getExpiresIn());
48+
OneTimeToken ott = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
4949
this.oneTimeTokenByToken.put(token, ott);
5050
cleanExpiredTokensIfNeeded();
5151
return ott;

core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.sql.Timestamp;
2222
import java.sql.Types;
2323
import java.time.Clock;
24-
import java.time.Duration;
2524
import java.time.Instant;
2625
import java.util.ArrayList;
2726
import java.util.List;
@@ -132,8 +131,8 @@ public void setCleanupCron(String cleanupCron) {
132131
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
133132
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
134133
String token = UUID.randomUUID().toString();
135-
Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
136-
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
134+
Instant expiresAt = this.clock.instant().plusSeconds(request.getExpiresIn());
135+
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
137136
insertOneTimeToken(oneTimeToken);
138137
return oneTimeToken;
139138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.web.authentication.ott;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
21+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
22+
import org.springframework.util.Assert;
23+
import org.springframework.util.StringUtils;
24+
25+
/**
26+
* Default implementation of {@link GenerateOneTimeTokenRequestResolver}. Resolves
27+
* {@link GenerateOneTimeTokenRequest} from username parameter.
28+
*
29+
* @author Max Batischev
30+
* @since 6.5
31+
*/
32+
public final class DefaultGenerateOneTimeTokenRequestResolver implements GenerateOneTimeTokenRequestResolver {
33+
34+
private static final int DEFAULT_EXPIRES_IN = 300;
35+
36+
private int expiresIn = DEFAULT_EXPIRES_IN;
37+
38+
@Override
39+
public GenerateOneTimeTokenRequest resolve(HttpServletRequest request) {
40+
String username = request.getParameter("username");
41+
if (!StringUtils.hasText(username)) {
42+
return null;
43+
}
44+
return new GenerateOneTimeTokenRequest(username, this.expiresIn);
45+
}
46+
47+
/**
48+
* Sets one-time token expiration time (seconds)
49+
* @param expiresIn one-time token expiration time
50+
*/
51+
public void setExpiresIn(int expiresIn) {
52+
Assert.isTrue(expiresIn > 0, "expiresAt must be > 0");
53+
this.expiresIn = expiresIn;
54+
}
55+
56+
}

web/src/main/java/org/springframework/security/web/authentication/ott/GenerateOneTimeTokenFilter.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.springframework.security.authentication.ott.OneTimeTokenService;
3030
import org.springframework.security.web.util.matcher.RequestMatcher;
3131
import org.springframework.util.Assert;
32-
import org.springframework.util.StringUtils;
3332
import org.springframework.web.filter.OncePerRequestFilter;
3433

3534
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
@@ -49,6 +48,8 @@ public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
4948

5049
private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate");
5150

51+
private GenerateOneTimeTokenRequestResolver requestResolver = new DefaultGenerateOneTimeTokenRequestResolver();
52+
5253
public GenerateOneTimeTokenFilter(OneTimeTokenService tokenService,
5354
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler) {
5455
Assert.notNull(tokenService, "tokenService cannot be null");
@@ -64,12 +65,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
6465
filterChain.doFilter(request, response);
6566
return;
6667
}
67-
String username = request.getParameter("username");
68-
if (!StringUtils.hasText(username)) {
68+
GenerateOneTimeTokenRequest generateRequest = this.requestResolver.resolve(request);
69+
if (generateRequest == null) {
6970
filterChain.doFilter(request, response);
7071
return;
7172
}
72-
GenerateOneTimeTokenRequest generateRequest = new GenerateOneTimeTokenRequest(username);
7373
OneTimeToken ott = this.tokenService.generate(generateRequest);
7474
this.tokenGenerationSuccessHandler.handle(request, response, ott);
7575
}
@@ -83,4 +83,15 @@ public void setRequestMatcher(RequestMatcher requestMatcher) {
8383
this.requestMatcher = requestMatcher;
8484
}
8585

86+
/**
87+
* Use the given {@link GenerateOneTimeTokenRequestResolver} to resolve
88+
* {@link GenerateOneTimeTokenRequest}.
89+
* @param requestResolver {@link GenerateOneTimeTokenRequestResolver}
90+
* @since 6.5
91+
*/
92+
public void setRequestResolver(GenerateOneTimeTokenRequestResolver requestResolver) {
93+
Assert.notNull(requestResolver, "requestResolver cannot be null");
94+
this.requestResolver = requestResolver;
95+
}
96+
8697
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.web.authentication.ott;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
23+
24+
/**
25+
* A strategy for resolving a {@link GenerateOneTimeTokenRequest} from the
26+
* {@link HttpServletRequest}.
27+
*
28+
* @author Max Batischev
29+
* @since 6.5
30+
*/
31+
public interface GenerateOneTimeTokenRequestResolver {
32+
33+
/**
34+
* Resolves {@link GenerateOneTimeTokenRequest} from {@link HttpServletRequest}
35+
* @param request {@link HttpServletRequest} to resolve
36+
* @return {@link GenerateOneTimeTokenRequest}
37+
*/
38+
@Nullable
39+
GenerateOneTimeTokenRequest resolve(HttpServletRequest request);
40+
41+
}

0 commit comments

Comments
 (0)