1
0
mirror of synced 2026-05-22 21:33:16 +00:00

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
This commit is contained in:
Rob Winch
2025-01-31 16:47:50 -06:00
parent b56650100a
commit 10394c8f2a
2 changed files with 88 additions and 50 deletions
@@ -19,7 +19,6 @@ package org.springframework.security.config.annotation.web.configurers.ott;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneOffset;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; 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.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken; 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.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 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.GenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatException; 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.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
@@ -72,6 +77,15 @@ public class OneTimeTokenLoginConfigurerTests {
@Autowired(required = false) @Autowired(required = false)
MockMvc mvc; MockMvc mvc;
@Autowired(required = false)
private GenerateOneTimeTokenRequestResolver resolver;
@Autowired(required = false)
private OneTimeTokenService tokenService;
@Autowired(required = false)
private OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler;
@Test @Test
void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception { void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception {
this.spring.register(OneTimeTokenDefaultConfig.class).autowire(); this.spring.register(OneTimeTokenDefaultConfig.class).autowire();
@@ -202,21 +216,18 @@ public class OneTimeTokenLoginConfigurerTests {
@Test @Test
void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception { void oneTimeTokenWhenCustomTokenExpirationTimeSetThenAuthenticate() throws Exception {
this.spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime.class).autowire(); this.spring.register(OneTimeTokenConfigWithCustomImpls.class).autowire();
this.mvc.perform(post("/ott/generate").param("username", "user").with(csrf())) GenerateOneTimeTokenRequest expectedGenerateRequest = new GenerateOneTimeTokenRequest("username-123",
.andExpectAll(status().isFound(), redirectedUrl("/login/ott")); 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(); verify(this.resolver).resolve(any());
verify(this.tokenService).generate(expectedGenerateRequest);
this.mvc.perform(post("/login/ott").param("token", token.getTokenValue()).with(csrf())) verify(this.tokenGenerationSuccessHandler).handle(any(), any(), eq(ott));
.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;
} }
private OneTimeToken getLastToken() { private OneTimeToken getLastToken() {
@@ -228,17 +239,21 @@ public class OneTimeTokenLoginConfigurerTests {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@EnableWebSecurity @EnableWebSecurity
@Import(UserDetailsServiceConfig.class) @Import(UserDetailsServiceConfig.class)
static class OneTimeTokenConfigWithCustomTokenExpirationTime { static class OneTimeTokenConfigWithCustomImpls {
@Bean @Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityFilterChain securityFilterChain(HttpSecurity http,
GenerateOneTimeTokenRequestResolver ottRequestResolver, OneTimeTokenService ottTokenService,
OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception { OneTimeTokenGenerationSuccessHandler ottSuccessHandler) throws Exception {
// @formatter:off // @formatter:off
http http
.authorizeHttpRequests((authz) -> authz .authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oneTimeTokenLogin((ott) -> ott .oneTimeTokenLogin((ott) -> ott
.generateRequestResolver(ottRequestResolver)
.tokenService(ottTokenService)
.tokenGenerationSuccessHandler(ottSuccessHandler) .tokenGenerationSuccessHandler(ottSuccessHandler)
); );
// @formatter:on // @formatter:on
@@ -246,17 +261,18 @@ public class OneTimeTokenLoginConfigurerTests {
} }
@Bean @Bean
TestOneTimeTokenGenerationSuccessHandler ottSuccessHandler() { GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
return new TestOneTimeTokenGenerationSuccessHandler(); return mock(GenerateOneTimeTokenRequestResolver.class);
} }
@Bean @Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() { OneTimeTokenService ottService() {
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver(); return mock(OneTimeTokenService.class);
return (request) -> { }
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600)); @Bean
}; OneTimeTokenGenerationSuccessHandler ottSuccessHandler() {
return mock(OneTimeTokenGenerationSuccessHandler.class);
} }
} }
@@ -16,6 +16,10 @@
package org.springframework.security.config.annotation.web 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.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.assertj.core.api.Assertions.assertThat 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.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import 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.OneTimeToken
import org.springframework.security.authentication.ott.OneTimeTokenService
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.test.SpringTestContext 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.SecurityFilterChain
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
import org.springframework.security.web.authentication.ott.DefaultGenerateOneTimeTokenRequestResolver 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.OneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
@@ -60,6 +68,15 @@ class OneTimeTokenLoginDslTests {
@Autowired @Autowired
private lateinit var mockMvc: MockMvc 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 @Test
fun `oneTimeToken when correct token then can authenticate`() { fun `oneTimeToken when correct token then can authenticate`() {
spring.register(OneTimeTokenConfig::class.java).autowire() spring.register(OneTimeTokenConfig::class.java).autowire()
@@ -110,29 +127,22 @@ class OneTimeTokenLoginDslTests {
} }
@Test @Test
fun `oneTimeToken when custom resolver set then use custom token`() { fun `oneTimeToken when custom impls set then used`() {
spring.register(OneTimeTokenConfigWithCustomTokenResolver::class.java).autowire() 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( this.mockMvc.perform(
MockMvcRequestBuilders.post("/ott/generate").param("username", "user") MockMvcRequestBuilders.post("/ott/generate").param("username", "user")
.with(SecurityMockMvcRequestPostProcessors.csrf()) .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 { private fun getLastToken(): OneTimeToken {
@@ -170,20 +180,22 @@ class OneTimeTokenLoginDslTests {
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@Import(UserDetailsServiceConfig::class) @Import(UserDetailsServiceConfig::class)
open class OneTimeTokenConfigWithCustomTokenResolver { open class OneTimeTokenConfigWithCustomImpls {
@Bean @Bean
open fun securityFilterChain(http: HttpSecurity, ottSuccessHandler: OneTimeTokenGenerationSuccessHandler): SecurityFilterChain { open fun securityFilterChain(http: HttpSecurity,
ottRequestResolver: GenerateOneTimeTokenRequestResolver,
ottService: OneTimeTokenService,
ottSuccessHandler: OneTimeTokenGenerationSuccessHandler): SecurityFilterChain {
// @formatter:off // @formatter:off
http { http {
authorizeHttpRequests { authorizeHttpRequests {
authorize(anyRequest, authenticated) authorize(anyRequest, authenticated)
} }
oneTimeTokenLogin { oneTimeTokenLogin {
generateRequestResolver = ottRequestResolver
tokenService = ottService
oneTimeTokenGenerationSuccessHandler = ottSuccessHandler oneTimeTokenGenerationSuccessHandler = ottSuccessHandler
generateRequestResolver = DefaultGenerateOneTimeTokenRequestResolver().apply {
this.setExpiresIn(Duration.ofMinutes(10))
}
} }
} }
// @formatter:on // @formatter:on
@@ -191,8 +203,18 @@ class OneTimeTokenLoginDslTests {
} }
@Bean @Bean
open fun ottSuccessHandler(): TestOneTimeTokenGenerationSuccessHandler { open fun ottRequestResolver(): GenerateOneTimeTokenRequestResolver {
return TestOneTimeTokenGenerationSuccessHandler() return mockk()
}
@Bean
open fun ottService(): OneTimeTokenService {
return mockk()
}
@Bean
open fun ottSuccessHandler(): OneTimeTokenGenerationSuccessHandler {
return mockk()
} }
} }