diff --git a/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java index b83f46081e..d475d2c7c6 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OneTimeTokenLoginSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; @@ -40,6 +42,8 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver; +import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver; import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler; import org.springframework.test.web.reactive.server.WebTestClient; @@ -49,6 +53,8 @@ import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Tests for {@link ServerHttpSecurity.OneTimeTokenLoginSpec} @@ -107,7 +113,7 @@ public class OneTimeTokenLoginSpecTests { .expectHeader().valueEquals("Location", "/login/ott"); // @formatter:on - String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue(); + String token = getLastToken().getTokenValue(); // @formatter:off this.client.mutateWith(SecurityMockServerConfigurers.csrf()) @@ -143,7 +149,7 @@ public class OneTimeTokenLoginSpecTests { .expectHeader().valueEquals("Location", "/redirected"); // @formatter:on - String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue(); + String token = getLastToken().getTokenValue(); // @formatter:off this.client.mutateWith(SecurityMockServerConfigurers.csrf()) @@ -179,7 +185,7 @@ public class OneTimeTokenLoginSpecTests { .expectHeader().valueEquals("Location", "/login/ott"); // @formatter:on - String token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken.getTokenValue(); + String token = getLastToken().getTokenValue(); // @formatter:off this.client.mutateWith(SecurityMockServerConfigurers.csrf()) @@ -268,18 +274,49 @@ public class OneTimeTokenLoginSpecTests { assertThat(response.contains(GENERATE_OTT_PART)).isTrue(); } + private OneTimeToken getLastToken() { + OneTimeToken lastToken = this.spring.getContext() + .getBean(TestServerOneTimeTokenGenerationSuccessHandler.class).lastToken; + return lastToken; + } + @Test void oneTimeTokenWhenNoOneTimeTokenGenerationSuccessHandlerThenException() { assertThatException() - .isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire()) - .havingRootCause() - .isInstanceOf(IllegalStateException.class) - .withMessage(""" + .isThrownBy(() -> this.spring.register(OneTimeTokenNotGeneratedOttHandlerConfig.class).autowire()) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage(""" A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin(). Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL. """); } + @Test + void oneTimeTokenWhenCustomRequestResolverSetThenCustomResolverUse() { + this.spring.register(OneTimeTokenConfigWithCustomRequestResolver.class).autowire(); + + // @formatter:off + this.client.mutateWith(SecurityMockServerConfigurers.csrf()) + .post() + .uri((uriBuilder) -> uriBuilder + .path("/ott/generate") + .build() + ) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("username", "user")) + .exchange() + .expectStatus() + .is3xxRedirection() + .expectHeader().valueEquals("Location", "/login/ott"); + // @formatter:on + + ServerGenerateOneTimeTokenRequestResolver resolver = this.spring.getContext() + .getBean(ServerGenerateOneTimeTokenRequestResolver.class); + + verify(resolver, times(1)).resolve(ArgumentMatchers.any(ServerWebExchange.class)); + } + @Configuration(proxyBeanMethods = false) @EnableWebFlux @EnableWebFluxSecurity @@ -287,7 +324,8 @@ public class OneTimeTokenLoginSpecTests { static class OneTimeTokenDefaultConfig { @Bean - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, + ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) { // @formatter:off http .authorizeExchange((authorize) -> authorize @@ -295,12 +333,17 @@ public class OneTimeTokenLoginSpecTests { .authenticated() ) .oneTimeTokenLogin((ott) -> ott - .tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler()) + .tokenGenerationSuccessHandler(ottSuccessHandler) ); // @formatter:on return http.build(); } + @Bean + TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestServerOneTimeTokenGenerationSuccessHandler(); + } + } @Configuration(proxyBeanMethods = false) @@ -310,7 +353,8 @@ public class OneTimeTokenLoginSpecTests { static class OneTimeTokenDifferentUrlsConfig { @Bean - SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, + ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) { // @formatter:off http .authorizeExchange((authorize) -> authorize @@ -319,7 +363,7 @@ public class OneTimeTokenLoginSpecTests { ) .oneTimeTokenLogin((ott) -> ott .tokenGeneratingUrl("/generateurl") - .tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler("/redirected")) + .tokenGenerationSuccessHandler(ottSuccessHandler) .loginProcessingUrl("/loginprocessingurl") .authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/authenticated")) ); @@ -327,6 +371,11 @@ public class OneTimeTokenLoginSpecTests { return http.build(); } + @Bean + TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestServerOneTimeTokenGenerationSuccessHandler("/redirected"); + } + } @Configuration(proxyBeanMethods = false) @@ -336,7 +385,8 @@ public class OneTimeTokenLoginSpecTests { static class OneTimeTokenFormLoginConfig { @Bean - SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, + ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) { // @formatter:off http .authorizeExchange((authorize) -> authorize @@ -345,12 +395,17 @@ public class OneTimeTokenLoginSpecTests { ) .formLogin(Customizer.withDefaults()) .oneTimeTokenLogin((ott) -> ott - .tokenGenerationSuccessHandler(new TestServerOneTimeTokenGenerationSuccessHandler()) + .tokenGenerationSuccessHandler(ottSuccessHandler) ); // @formatter:on return http.build(); } + @Bean + TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestServerOneTimeTokenGenerationSuccessHandler(); + } + } @Configuration(proxyBeanMethods = false) @@ -385,10 +440,44 @@ public class OneTimeTokenLoginSpecTests { } + @Configuration(proxyBeanMethods = false) + @EnableWebFlux + @EnableWebFluxSecurity + @Import(UserDetailsServiceConfig.class) + static class OneTimeTokenConfigWithCustomRequestResolver { + + @Bean + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, + ServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize + .anyExchange() + .authenticated() + ) + .oneTimeTokenLogin((ott) -> ott + .tokenGenerationSuccessHandler(ottSuccessHandler) + ); + // @formatter:on + return http.build(); + } + + @Bean + ServerGenerateOneTimeTokenRequestResolver resolver() { + return Mockito.spy(new DefaultServerGenerateOneTimeTokenRequestResolver()); + } + + @Bean + TestServerOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return new TestServerOneTimeTokenGenerationSuccessHandler(); + } + + } + private static class TestServerOneTimeTokenGenerationSuccessHandler implements ServerOneTimeTokenGenerationSuccessHandler { - private static OneTimeToken lastToken; + private OneTimeToken lastToken; private final ServerOneTimeTokenGenerationSuccessHandler delegate; @@ -402,7 +491,7 @@ public class OneTimeTokenLoginSpecTests { @Override public Mono handle(ServerWebExchange exchange, OneTimeToken oneTimeToken) { - lastToken = oneTimeToken; + this.lastToken = oneTimeToken; return this.delegate.handle(exchange, oneTimeToken); } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt index 674a1ec570..61958d476c 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDslTests.kt @@ -16,18 +16,17 @@ package org.springframework.security.config.web.server -import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import reactor.core.publisher.Mono - +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.Mockito.verify import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import -import org.springframework.context.ApplicationContext import org.springframework.http.MediaType -import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest import org.springframework.security.authentication.ott.OneTimeToken import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.test.SpringTestContext @@ -36,10 +35,10 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi import org.springframework.security.core.userdetails.ReactiveUserDetailsService import org.springframework.security.core.userdetails.User import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers -import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver -import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler +import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver +import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler import org.springframework.security.web.server.authentication.ott.ServerRedirectOneTimeTokenGenerationSuccessHandler import org.springframework.test.web.reactive.server.WebTestClient @@ -47,9 +46,7 @@ import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.function.BodyInserters import org.springframework.web.server.ServerWebExchange import org.springframework.web.util.UriBuilder -import java.time.Duration -import java.time.Instant -import java.time.ZoneOffset +import reactor.core.publisher.Mono /** * Tests for [ServerOneTimeTokenLoginDsl] @@ -102,7 +99,7 @@ class ServerOneTimeTokenLoginDslTests { .is3xxRedirection() .expectHeader().valueEquals("Location", "/login/ott") - val token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue + val token = lastToken()!!.tokenValue client.mutateWith(SecurityMockServerConfigurers.csrf()) .post() @@ -136,7 +133,7 @@ class ServerOneTimeTokenLoginDslTests { .is3xxRedirection() .expectHeader().valueEquals("Location", "/redirected") - val token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken?.tokenValue + val token = lastToken()!!.tokenValue client.mutateWith(SecurityMockServerConfigurers.csrf()) .post() @@ -154,8 +151,8 @@ class ServerOneTimeTokenLoginDslTests { } @Test - fun `oneTimeToken when custom token expiration time set then authenticate`() { - spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime::class.java).autowire() + fun `oneTimeToken when custom request resolver set then custom resolver use`() { + spring.register(OneTimeTokenConfigWithCustomRequestResolver::class.java).autowire() // @formatter:off client.mutateWith(SecurityMockServerConfigurers.csrf()) @@ -171,29 +168,18 @@ class ServerOneTimeTokenLoginDslTests { .is3xxRedirection() .expectHeader().valueEquals("Location", "/login/ott") - client.mutateWith(SecurityMockServerConfigurers.csrf()) - .post() - .uri{ uriBuilder:UriBuilder -> uriBuilder - .path("/ott/generate") - .build() - } - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(BodyInserters.fromFormData("username", "user")) - .exchange() - .expectStatus() - .is3xxRedirection() - .expectHeader().valueEquals("Location", "/login/ott") + val resolver = spring.context + .getBean(ServerGenerateOneTimeTokenRequestResolver::class.java) - val token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken - - Assertions.assertThat(getCurrentMinutes(token!!.expiresAt)).isEqualTo(10) + verify(resolver, Mockito.times(1)) + .resolve(ArgumentMatchers.any(ServerWebExchange::class.java)) + // @formatter:on } - private fun getCurrentMinutes(expiresAt:Instant): Int { - val expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).minute - val currentMinutes = Instant.now().atZone(ZoneOffset.UTC).minute - return expiresMinutes - currentMinutes - } + private fun lastToken():OneTimeToken? = + spring.context.getBean(TestServerOneTimeTokenGenerationSuccessHandler::class.java) + .lastToken + @Configuration @EnableWebFlux @@ -202,18 +188,23 @@ class ServerOneTimeTokenLoginDslTests { open class OneTimeTokenConfig { @Bean - open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + open fun springWebFilterChain(http: ServerHttpSecurity, + ottSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler): SecurityWebFilterChain { // @formatter:off return http { authorizeExchange { authorize(anyExchange, authenticated) } oneTimeTokenLogin { - tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler() + tokenGenerationSuccessHandler = ottSuccessHandler } } // @formatter:on } + + @Bean + open fun ottSuccessHandler(): ServerOneTimeTokenGenerationSuccessHandler = + TestServerOneTimeTokenGenerationSuccessHandler() } @Configuration @@ -223,7 +214,8 @@ class ServerOneTimeTokenLoginDslTests { open class OneTimeTokenDifferentUrlsConfig { @Bean - open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + open fun springWebFilterChain(http: ServerHttpSecurity, + ottSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler): SecurityWebFilterChain { // @formatter:off return http { authorizeExchange { @@ -231,13 +223,17 @@ class ServerOneTimeTokenLoginDslTests { } oneTimeTokenLogin { tokenGeneratingUrl = "/generateurl" - tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler("/redirected") + tokenGenerationSuccessHandler = ottSuccessHandler loginProcessingUrl = "/loginprocessingurl" authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/authenticated") } } // @formatter:on } + + @Bean + open fun ottSuccessHandler(): ServerOneTimeTokenGenerationSuccessHandler = + TestServerOneTimeTokenGenerationSuccessHandler("/redirected") } @Configuration(proxyBeanMethods = false) @@ -252,36 +248,33 @@ class ServerOneTimeTokenLoginDslTests { @EnableWebFlux @EnableWebFluxSecurity @Import(OneTimeTokenLoginSpecTests.UserDetailsServiceConfig::class) - open class OneTimeTokenConfigWithCustomTokenExpirationTime { + open class OneTimeTokenConfigWithCustomRequestResolver { @Bean - open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + open fun securityWebFilterChain(http: ServerHttpSecurity, + ottSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler): SecurityWebFilterChain { // @formatter:off return http { authorizeExchange { authorize(anyExchange, authenticated) } oneTimeTokenLogin { - tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler() + tokenGenerationSuccessHandler = ottSuccessHandler } } } @Bean - open fun resolver(): ServerGenerateOneTimeTokenRequestResolver { - val resolver = DefaultServerGenerateOneTimeTokenRequestResolver() - return ServerGenerateOneTimeTokenRequestResolver { exchange -> - resolver.resolve(exchange) - .map { request -> GenerateOneTimeTokenRequest(request.username, Duration.ofSeconds(600)) } - } - } + open fun resolver(): ServerGenerateOneTimeTokenRequestResolver = + Mockito.spy(DefaultServerGenerateOneTimeTokenRequestResolver()) + + @Bean + open fun ottSuccessHandler(): ServerOneTimeTokenGenerationSuccessHandler = + TestServerOneTimeTokenGenerationSuccessHandler() } private class TestServerOneTimeTokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler { private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null - - companion object { - var lastToken: OneTimeToken? = null - } + var lastToken: OneTimeToken? = null constructor() { this.delegate = ServerRedirectOneTimeTokenGenerationSuccessHandler("/login/ott")