From 10394c8f2a9bde890cd1eabd685222a97421c9c3 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:47:50 -0600 Subject: [PATCH] OTT Tests use Mocks Instead of Comparing Expires Previously, expires was compared to test if a custom implementations were used. Now the tests verify this through mocks. Closes gh-16515 --- .../ott/OneTimeTokenLoginConfigurerTests.java | 68 +++++++++++------- .../web/OneTimeTokenLoginDslTests.kt | 70 ++++++++++++------- 2 files changed, 88 insertions(+), 50 deletions(-) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java index 2d3441e674..a5f8417892 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java @@ -19,7 +19,6 @@ package org.springframework.security.config.annotation.web.configurers.ott; import java.io.IOException; import java.time.Duration; import java.time.Instant; -import java.time.ZoneOffset; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -32,8 +31,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.ott.DefaultOneTimeToken; import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; import org.springframework.security.authentication.ott.OneTimeToken; +import org.springframework.security.authentication.ott.OneTimeTokenService; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -44,7 +45,6 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver; import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver; import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; @@ -55,6 +55,11 @@ import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; @@ -72,6 +77,15 @@ public class OneTimeTokenLoginConfigurerTests { @Autowired(required = false) MockMvc mvc; + @Autowired(required = false) + private GenerateOneTimeTokenRequestResolver resolver; + + @Autowired(required = false) + private OneTimeTokenService tokenService; + + @Autowired(required = false) + private OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler; + @Test void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception { this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); @@ -202,21 +216,18 @@ public class OneTimeTokenLoginConfigurerTests { @Test void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception { - this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire(); - this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) - .andExpectAll(status().isFound(), redirectedUrl("/login/ott")); + this.spring.register(OneTimeTokenConfigWithCustomImpls.class).autowire(); + GenerateOneTimeTokenRequest expectedGenerateRequest = new GenerateOneTimeTokenRequest("username-123", + Duration.ofMinutes(10)); + OneTimeToken ott = new DefaultOneTimeToken("token-123", expectedGenerateRequest.getUsername(), + Instant.now().plus(expectedGenerateRequest.getExpiresIn())); + given(this.resolver.resolve(any())).willReturn(expectedGenerateRequest); + given(this.tokenService.generate(expectedGenerateRequest)).willReturn(ott); + this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())); - OneTimeToken token = getLastToken(); - - this.mvc.perform(post("/login/ott").param("token", token.getTokenValue()).with(csrf())) - .andExpectAll(status().isFound(), redirectedUrl("/"), authenticated()); - assertThat(getCurrentMinutes(token.getExpiresAt())).isEqualTo(10); - } - - private int getCurrentMinutes(Instant expiresAt) { - int expiresMinutes = expiresAt.atZone(ZoneOffset.UTC).getMinute(); - int currentMinutes = Instant.now().atZone(ZoneOffset.UTC).getMinute(); - return expiresMinutes - currentMinutes; + verify(this.resolver).resolve(any()); + verify(this.tokenService).generate(expectedGenerateRequest); + verify(this.tokenGenerationSuccessHandler).handle(any(), any(), eq(ott)); } private OneTimeToken getLastToken() { @@ -228,17 +239,21 @@ public class OneTimeTokenLoginConfigurerTests { @Configuration(proxyBeanMethods = false) @EnableWebSecurity @Import(UserDetailsServiceConfig.class) - static class OneTimeTokenConfigWithCustomTokenExpirationTime { + static class OneTimeTokenConfigWithCustomImpls { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http, + GenerateOneTimeTokenRequestResolver ottRequestResolver, OneTimeTokenService ottTokenService, OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { + // @formatter:off - http + http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .oneTimeTokenLogin((ott) -> ott + .generateRequestResolver(ottRequestResolver) + .tokenService(ottTokenService) .tokenGenerationSuccessHandler(ottSuccessHandler) ); // @formatter:on @@ -246,17 +261,18 @@ public class OneTimeTokenLoginConfigurerTests { } @Bean - TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { - return new TestOneTimeTokenGenerationSuccessHandler(); + GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() { + return mock(GenerateOneTimeTokenRequestResolver.class); } @Bean - GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() { - DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver(); - return (request) -> { - GenerateOneTimeTokenRequest generate = delegate.resolve(request); - return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600)); - }; + OneTimeTokenService ottService() { + return mock(OneTimeTokenService.class); + } + + @Bean + OneTimeTokenGenerationSuccessHandler ottSuccessHandler() { + return mock(OneTimeTokenGenerationSuccessHandler.class); } } diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt index 12bacc47e2..8da71cd998 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OneTimeTokenLoginDslTests.kt @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.assertj.core.api.Assertions.assertThat @@ -25,7 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import +import org.springframework.security.authentication.ott.DefaultOneTimeToken +import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest import org.springframework.security.authentication.ott.OneTimeToken +import org.springframework.security.authentication.ott.OneTimeTokenService import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.test.SpringTestContext @@ -38,6 +45,7 @@ import org.springframework.security.test.web.servlet.response.SecurityMockMvcRes import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver +import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler import org.springframework.test.web.servlet.MockMvc @@ -60,6 +68,15 @@ class OneTimeTokenLoginDslTests { @Autowired private lateinit var mockMvc: MockMvc + @Autowired(required = false) + private lateinit var resolver: GenerateOneTimeTokenRequestResolver + + @Autowired(required = false) + private lateinit var tokenService: OneTimeTokenService + + @Autowired(required = false) + private lateinit var tokenGenerationSuccessHandler: OneTimeTokenGenerationSuccessHandler + @Test fun `oneTimeToken when correct token then can authenticate`() { spring.register(OneTimeTokenConfig::class.java).autowire() @@ -110,29 +127,22 @@ class OneTimeTokenLoginDslTests { } @Test - fun `oneTimeToken when custom resolver set then use custom token`() { - spring.register(OneTimeTokenConfigWithCustomTokenResolver::class.java).autowire() - + fun `oneTimeToken when custom impls set then used`() { + spring.register(OneTimeTokenConfigWithCustomImpls::class.java).autowire() + val expectedGenerateRequest = GenerateOneTimeTokenRequest("username-123", Duration.ofMinutes(10)); + val ott = DefaultOneTimeToken("token-123", expectedGenerateRequest.username, Instant.now().plus(expectedGenerateRequest.expiresIn)) + every { resolver.resolve(any()) } returns expectedGenerateRequest + every { tokenService.generate(expectedGenerateRequest) } returns ott + justRun { tokenGenerationSuccessHandler.handle(any(), any(), eq(ott)) } this.mockMvc.perform( MockMvcRequestBuilders.post("/ott/generate").param("username", "user") .with(SecurityMockMvcRequestPostProcessors.csrf()) - ).andExpectAll( - MockMvcResultMatchers - .status() - .isFound(), - MockMvcResultMatchers - .redirectedUrl("/login/ott") ) - val token = getLastToken() + verify { resolver.resolve(any()) } + verify { tokenService.generate(expectedGenerateRequest) } + verify { tokenGenerationSuccessHandler.handle(any(), any(), eq(ott)) } - assertThat(getCurrentMinutes(token!!.expiresAt)).isEqualTo(10) - } - - 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 getLastToken(): OneTimeToken { @@ -170,20 +180,22 @@ class OneTimeTokenLoginDslTests { @Configuration @EnableWebSecurity @Import(UserDetailsServiceConfig::class) - open class OneTimeTokenConfigWithCustomTokenResolver { + open class OneTimeTokenConfigWithCustomImpls { @Bean - open fun securityFilterChain(http: HttpSecurity, ottSuccessHandler: OneTimeTokenGenerationSuccessHandler): SecurityFilterChain { + open fun securityFilterChain(http: HttpSecurity, + ottRequestResolver: GenerateOneTimeTokenRequestResolver, + ottService: OneTimeTokenService, + ottSuccessHandler: OneTimeTokenGenerationSuccessHandler): SecurityFilterChain { // @formatter:off http { authorizeHttpRequests { authorize(anyRequest, authenticated) } oneTimeTokenLogin { + generateRequestResolver = ottRequestResolver + tokenService = ottService oneTimeTokenGenerationSuccessHandler = ottSuccessHandler - generateRequestResolver = DefaultGenerateOneTimeTokenRequestResolver().apply { - this.setExpiresIn(Duration.ofMinutes(10)) - } } } // @formatter:on @@ -191,8 +203,18 @@ class OneTimeTokenLoginDslTests { } @Bean - open fun ottSuccessHandler(): TestOneTimeTokenGenerationSuccessHandler { - return TestOneTimeTokenGenerationSuccessHandler() + open fun ottRequestResolver(): GenerateOneTimeTokenRequestResolver { + return mockk() + } + + @Bean + open fun ottService(): OneTimeTokenService { + return mockk() + } + + @Bean + open fun ottSuccessHandler(): OneTimeTokenGenerationSuccessHandler { + return mockk() } }