Skip to content

Commit bb60ff1

Browse files
Merge branch 'spring-projects:main' into main
2 parents 361347f + 8e2a4bf commit bb60ff1

File tree

21 files changed

+541
-134
lines changed

21 files changed

+541
-134
lines changed

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

Lines changed: 34 additions & 1 deletion
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.
@@ -29,6 +29,7 @@
2929
import java.util.Iterator;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Objects;
3233
import java.util.UUID;
3334
import java.util.function.Consumer;
3435
import java.util.function.Function;
@@ -53,6 +54,7 @@
5354
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
5455
import org.springframework.security.authentication.ReactiveAuthenticationManager;
5556
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
57+
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
5658
import org.springframework.security.authentication.ott.OneTimeToken;
5759
import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
5860
import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager;
@@ -156,7 +158,9 @@
156158
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
157159
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
158160
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
161+
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
159162
import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter;
163+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
160164
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter;
161165
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
162166
import org.springframework.security.web.server.authorization.AuthorizationContext;
@@ -5940,6 +5944,8 @@ public final class OneTimeTokenLoginSpec {
59405944

59415945
private ServerSecurityContextRepository securityContextRepository;
59425946

5947+
private ServerGenerateOneTimeTokenRequestResolver requestResolver;
5948+
59435949
private String loginProcessingUrl = "/login/ott";
59445950

59455951
private String defaultSubmitPageUrl = "/login/ott";
@@ -5985,6 +5991,7 @@ private void configureOttGenerateFilter(ServerHttpSecurity http) {
59855991
getTokenGenerationSuccessHandler());
59865992
generateFilter
59875993
.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
5994+
generateFilter.setGenerateRequestResolver(getRequestResolver());
59885995
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
59895996
}
59905997

@@ -6112,6 +6119,32 @@ public OneTimeTokenLoginSpec authenticationConverter(ServerAuthenticationConvert
61126119
return this;
61136120
}
61146121

6122+
/**
6123+
* Use this {@link ServerGenerateOneTimeTokenRequestResolver} when resolving
6124+
* {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}. By default,
6125+
* the {@link DefaultServerGenerateOneTimeTokenRequestResolver} is used.
6126+
* @param requestResolver the
6127+
* {@link DefaultServerGenerateOneTimeTokenRequestResolver} to use
6128+
* @since 6.5
6129+
*/
6130+
public OneTimeTokenLoginSpec generateRequestResolver(
6131+
ServerGenerateOneTimeTokenRequestResolver requestResolver) {
6132+
Assert.notNull(requestResolver, "generateRequestResolver cannot be null");
6133+
this.requestResolver = requestResolver;
6134+
return this;
6135+
}
6136+
6137+
private ServerGenerateOneTimeTokenRequestResolver getRequestResolver() {
6138+
if (this.requestResolver != null) {
6139+
return this.requestResolver;
6140+
}
6141+
ServerGenerateOneTimeTokenRequestResolver bean = getBeanOrNull(
6142+
ServerGenerateOneTimeTokenRequestResolver.class);
6143+
this.requestResolver = Objects.requireNonNullElseGet(bean,
6144+
DefaultServerGenerateOneTimeTokenRequestResolver::new);
6145+
return this.requestResolver;
6146+
}
6147+
61156148
/**
61166149
* Specifies the URL to process the login request, defaults to {@code /login/ott}.
61176150
* Only POST requests are processed, for that reason make sure that you pass a

config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt

Lines changed: 5 additions & 1 deletion
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.
@@ -18,6 +18,7 @@ package org.springframework.security.config.web.server
1818

1919
import org.springframework.security.authentication.ReactiveAuthenticationManager
2020
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
21+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver
2122
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
2223
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
2324
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
@@ -34,6 +35,7 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo
3435
* @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
3536
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
3637
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used
38+
* @property generateRequestResolver the [ServerGenerateOneTimeTokenRequestResolver] to be used
3739
* @property defaultSubmitPageUrl sets the URL that the default submit page will be generated
3840
* @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown
3941
* @property loginProcessingUrl the URL to process the login request
@@ -50,6 +52,7 @@ class ServerOneTimeTokenLoginDsl {
5052
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
5153
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
5254
var securityContextRepository: ServerSecurityContextRepository? = null
55+
var generateRequestResolver: ServerGenerateOneTimeTokenRequestResolver? = null
5356
var defaultSubmitPageUrl: String? = null
5457
var loginProcessingUrl: String? = null
5558
var tokenGeneratingUrl: String? = null
@@ -71,6 +74,7 @@ class ServerOneTimeTokenLoginDsl {
7174
)
7275
}
7376
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
77+
generateRequestResolver?.also { oneTimeTokenLogin.generateRequestResolver(generateRequestResolver) }
7478
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
7579
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
7680
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }

config/src/test/java/org/springframework/security/SpringSecurityCoreVersionSerializableTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@
9999
import org.springframework.security.authentication.ott.InvalidOneTimeTokenException;
100100
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
101101
import org.springframework.security.authentication.password.CompromisedPasswordException;
102+
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
103+
import org.springframework.security.authorization.AuthorizationDecision;
104+
import org.springframework.security.authorization.AuthorizationDeniedException;
102105
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
103106
import org.springframework.security.cas.authentication.CasAuthenticationToken;
104107
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
@@ -471,6 +474,11 @@ class SpringSecurityCoreVersionSerializableTests {
471474
generatorByClassName.put(AbstractSessionEvent.class, (r) -> new AbstractSessionEvent(securityContext));
472475
generatorByClassName.put(SecurityConfig.class, (r) -> new SecurityConfig("value"));
473476
generatorByClassName.put(TransientSecurityContext.class, (r) -> new TransientSecurityContext(authentication));
477+
generatorByClassName.put(AuthorizationDeniedException.class,
478+
(r) -> new AuthorizationDeniedException("message", new AuthorizationDecision(false)));
479+
generatorByClassName.put(AuthorizationDecision.class, (r) -> new AuthorizationDecision(true));
480+
generatorByClassName.put(AuthorityAuthorizationDecision.class,
481+
(r) -> new AuthorityAuthorizationDecision(true, AuthorityUtils.createAuthorityList("ROLE_USER")));
474482

475483
// cas
476484
generatorByClassName.put(CasServiceTicketAuthenticationToken.class, (r) -> {

config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java

Lines changed: 101 additions & 12 deletions
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.
@@ -21,6 +21,8 @@
2121

2222
import org.junit.jupiter.api.Test;
2323
import org.junit.jupiter.api.extension.ExtendWith;
24+
import org.mockito.ArgumentMatchers;
25+
import org.mockito.Mockito;
2426
import reactor.core.publisher.Mono;
2527

2628
import org.springframework.beans.factory.annotation.Autowired;
@@ -40,6 +42,8 @@
4042
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers;
4143
import org.springframework.security.web.server.SecurityWebFilterChain;
4244
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
45+
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
46+
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
4347
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
4448
import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler;
4549
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -49,6 +53,8 @@
4953

5054
import static org.assertj.core.api.Assertions.assertThat;
5155
import static org.assertj.core.api.Assertions.assertThatException;
56+
import static org.mockito.Mockito.times;
57+
import static org.mockito.Mockito.verify;
5258

5359
/**
5460
* Tests for {@link ServerHttpSecurity.OneTimeTokenLoginSpec}
@@ -107,7 +113,7 @@ void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() {
107113
.expectHeader().valueEquals("Location", "/login/ott");
108114
// @formatter:on
109115

110-
String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue();
116+
String token = getLastToken().getTokenValue();
111117

112118
// @formatter:off
113119
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
@@ -143,7 +149,7 @@ void oneTimeTokenWhenDifferentAuthenticationUrlsThenCanAuthenticate() {
143149
.expectHeader().valueEquals("Location", "/redirected");
144150
// @formatter:on
145151

146-
String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue();
152+
String token = getLastToken().getTokenValue();
147153

148154
// @formatter:off
149155
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
@@ -179,7 +185,7 @@ void oneTimeTokenWhenCorrectTokenUsedTwiceThenSecondTimeFails() {
179185
.expectHeader().valueEquals("Location", "/login/ott");
180186
// @formatter:on
181187

182-
String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue();
188+
String token = getLastToken().getTokenValue();
183189

184190
// @formatter:off
185191
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
@@ -268,6 +274,12 @@ void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() {
268274
assertThat(response.contains(GENERATE_OTT_PART)).isTrue();
269275
}
270276

277+
private OneTimeToken getLastToken() {
278+
OneTimeToken lastToken = this.spring.getContext()
279+
.getBean(TestServerOneTimeTokenGenerationSuccessHandler.class).lastToken;
280+
return lastToken;
281+
}
282+
271283
@Test
272284
void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() {
273285
assertThatException()
@@ -280,27 +292,58 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
280292
""");
281293
}
282294

295+
@Test
296+
void oneTimeTokenWhenCustomRequestResolverSetThenCustomResolverUse() {
297+
this.spring.register(OneTimeTokenConfigWithCustomRequestResolver.class).autowire();
298+
299+
// @formatter:off
300+
this.client.mutateWith(SecurityMockServerConfigurers.csrf())
301+
.post()
302+
.uri((uriBuilder) -> uriBuilder
303+
.path("/ott/generate")
304+
.build()
305+
)
306+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
307+
.body(BodyInserters.fromFormData("username", "user"))
308+
.exchange()
309+
.expectStatus()
310+
.is3xxRedirection()
311+
.expectHeader().valueEquals("Location", "/login/ott");
312+
// @formatter:on
313+
314+
ServerGenerateOneTimeTokenRequestResolver resolver = this.spring.getContext()
315+
.getBean(ServerGenerateOneTimeTokenRequestResolver.class);
316+
317+
verify(resolver, times(1)).resolve(ArgumentMatchers.any(ServerWebExchange.class));
318+
}
319+
283320
@Configuration(proxyBeanMethods = false)
284321
@EnableWebFlux
285322
@EnableWebFluxSecurity
286323
@Import(UserDetailsServiceConfig.class)
287324
static class OneTimeTokenDefaultConfig {
288325

289326
@Bean
290-
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
327+
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http,
328+
ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) {
291329
// @formatter:off
292330
http
293331
.authorizeExchange((authorize) -> authorize
294332
.anyExchange()
295333
.authenticated()
296334
)
297335
.oneTimeTokenLogin((ott) -> ott
298-
.tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler())
336+
.tokenGenerationSuccessHandler(ottSuccessHandler)
299337
);
300338
// @formatter:on
301339
return http.build();
302340
}
303341

342+
@Bean
343+
TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
344+
return new TestServerOneTimeTokenGenerationSuccessHandler();
345+
}
346+
304347
}
305348

306349
@Configuration(proxyBeanMethods = false)
@@ -310,7 +353,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
310353
static class OneTimeTokenDifferentUrlsConfig {
311354

312355
@Bean
313-
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
356+
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http,
357+
ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) {
314358
// @formatter:off
315359
http
316360
.authorizeExchange((authorize) -> authorize
@@ -319,14 +363,19 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
319363
)
320364
.oneTimeTokenLogin((ott) -> ott
321365
.tokenGeneratingUrl("/generateurl")
322-
.tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler("/redirected"))
366+
.tokenGenerationSuccessHandler(ottSuccessHandler)
323367
.loginProcessingUrl("/loginprocessingurl")
324368
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/authenticated"))
325369
);
326370
// @formatter:on
327371
return http.build();
328372
}
329373

374+
@Bean
375+
TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
376+
return new TestServerOneTimeTokenGenerationSuccessHandler("/redirected");
377+
}
378+
330379
}
331380

332381
@Configuration(proxyBeanMethods = false)
@@ -336,7 +385,8 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
336385
static class OneTimeTokenFormLoginConfig {
337386

338387
@Bean
339-
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
388+
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http,
389+
ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) {
340390
// @formatter:off
341391
http
342392
.authorizeExchange((authorize) -> authorize
@@ -345,12 +395,17 @@ SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
345395
)
346396
.formLogin(Customizer.withDefaults())
347397
.oneTimeTokenLogin((ott) -> ott
348-
.tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler())
398+
.tokenGenerationSuccessHandler(ottSuccessHandler)
349399
);
350400
// @formatter:on
351401
return http.build();
352402
}
353403

404+
@Bean
405+
TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
406+
return new TestServerOneTimeTokenGenerationSuccessHandler();
407+
}
408+
354409
}
355410

356411
@Configuration(proxyBeanMethods = false)
@@ -385,10 +440,44 @@ ReactiveUserDetailsService userDetailsService() {
385440

386441
}
387442

443+
@Configuration(proxyBeanMethods = false)
444+
@EnableWebFlux
445+
@EnableWebFluxSecurity
446+
@Import(UserDetailsServiceConfig.class)
447+
static class OneTimeTokenConfigWithCustomRequestResolver {
448+
449+
@Bean
450+
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http,
451+
ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) {
452+
// @formatter:off
453+
http
454+
.authorizeExchange((authorize) -> authorize
455+
.anyExchange()
456+
.authenticated()
457+
)
458+
.oneTimeTokenLogin((ott) -> ott
459+
.tokenGenerationSuccessHandler(ottSuccessHandler)
460+
);
461+
// @formatter:on
462+
return http.build();
463+
}
464+
465+
@Bean
466+
ServerGenerateOneTimeTokenRequestResolver resolver() {
467+
return Mockito.spy(new DefaultServerGenerateOneTimeTokenRequestResolver());
468+
}
469+
470+
@Bean
471+
TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
472+
return new TestServerOneTimeTokenGenerationSuccessHandler();
473+
}
474+
475+
}
476+
388477
private static class TestServerOneTimeTokenGenerationSuccessHandler
389478
implements ServerOneTimeTokenGenerationSuccessHandler {
390479

391-
private static OneTimeToken lastToken;
480+
private OneTimeToken lastToken;
392481

393482
private final ServerOneTimeTokenGenerationSuccessHandler delegate;
394483

@@ -402,7 +491,7 @@ private static class TestServerOneTimeTokenGenerationSuccessHandler
402491

403492
@Override
404493
public Mono<Void> handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) {
405-
lastToken = oneTimeToken;
494+
this.lastToken = oneTimeToken;
406495
return this.delegate.handle(exchange, oneTimeToken);
407496
}
408497

0 commit comments

Comments
 (0)