Skip to content

Commit 714b2c4

Browse files
Add Support GenerateOneTimeTokenRequestResolver
Closes gh-16291
1 parent 036f6f2 commit 714b2c4

File tree

9 files changed

+274
-8
lines changed

9 files changed

+274
-8
lines changed

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

+28
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,27 @@ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
301308
return this.authenticationFailureHandler;
302309
}
303310

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

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

+53
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,54 @@ 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+
197250
@Configuration(proxyBeanMethods = false)
198251
@EnableWebSecurity
199252
@Import(UserDetailsServiceConfig.class)

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

+19
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,35 @@
2525
* @since 6.4
2626
*/
2727
public class GenerateOneTimeTokenRequest {
28+
private static final int DEFAULT_EXPIRES_IN = 300;
2829

2930
private final String username;
31+
private final int expiresIn;
3032

3133
public GenerateOneTimeTokenRequest(String username) {
3234
Assert.hasText(username, "username cannot be empty");
3335
this.username = username;
36+
this.expiresIn = DEFAULT_EXPIRES_IN;
37+
}
38+
39+
/**
40+
* Constructs an <code>GenerateOneTimeTokenRequest</code> with the specified username and expiresIn
41+
*
42+
* @param username username
43+
* @param expiresIn one-time token expiration time (seconds)
44+
*/
45+
public GenerateOneTimeTokenRequest(String username, int expiresIn) {
46+
Assert.hasText(username, "username cannot be empty");
47+
Assert.isTrue(expiresIn > 0, "expiresIn must be > 0");
48+
this.username = username;
49+
this.expiresIn = expiresIn;
3450
}
3551

3652
public String getUsername() {
3753
return this.username;
3854
}
3955

56+
public int getExpiresIn() {
57+
return this.expiresIn;
58+
}
4059
}

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-2
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ public void setCleanupCron(String cleanupCron) {
132132
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
133133
Assert.notNull(request, "generateOneTimeTokenRequest cannot be null");
134134
String token = UUID.randomUUID().toString();
135-
Instant fiveMinutesFromNow = this.clock.instant().plus(Duration.ofMinutes(5));
136-
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), fiveMinutesFromNow);
135+
Instant expiresAt = this.clock.instant().plusSeconds(request.getExpiresIn());
136+
OneTimeToken oneTimeToken = new DefaultOneTimeToken(token, request.getUsername(), expiresAt);
137137
insertOneTimeToken(oneTimeToken);
138138
return oneTimeToken;
139139
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 {@link GenerateOneTimeTokenRequest} from username parameter.
27+
*
28+
* @author Max Batischev
29+
* @since 6.5
30+
*/
31+
public final class DefaultGenerateOneTimeTokenRequestResolver implements GenerateOneTimeTokenRequestResolver {
32+
private static final int DEFAULT_EXPIRES_IN = 300;
33+
34+
private int expiresIn = DEFAULT_EXPIRES_IN;
35+
36+
@Override
37+
public GenerateOneTimeTokenRequest resolve(HttpServletRequest request) {
38+
String username = request.getParameter("username");
39+
if (!StringUtils.hasText(username)) {
40+
return null;
41+
}
42+
return new GenerateOneTimeTokenRequest(username, this.expiresIn);
43+
}
44+
45+
/**
46+
* Sets one-time token expiration time (seconds)
47+
*
48+
* @param expiresIn one-time token expiration time
49+
*/
50+
public void setExpiresIn(int expiresIn) {
51+
Assert.isTrue(expiresIn > 0, "expiresAt must be > 0");
52+
this.expiresIn = expiresIn;
53+
}
54+
}

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

+13-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,13 @@ public void setRequestMatcher(RequestMatcher requestMatcher) {
8383
this.requestMatcher = requestMatcher;
8484
}
8585

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

0 commit comments

Comments
 (0)