Skip to content

Commit

Permalink
OTT Tests use Mocks Instead of Comparing Expires
Browse files Browse the repository at this point in the history
Previously, expires was compared to test if a custom implementations
were used. Now the tests verify this through mocks.

Closes gh-16515
  • Loading branch information
rwinch committed Jan 31, 2025
1 parent b566501 commit 10394c8
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
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;
Expand All @@ -32,8 +31,10 @@
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;
Expand All @@ -44,7 +45,6 @@
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;
Expand All @@ -55,6 +55,11 @@

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;
Expand All @@ -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();
Expand Down Expand Up @@ -202,21 +216,18 @@ Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.

@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"));

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;
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()));

verify(this.resolver).resolve(any());
verify(this.tokenService).generate(expectedGenerateRequest);
verify(this.tokenGenerationSuccessHandler).handle(any(), any(), eq(ott));
}

private OneTimeToken getLastToken() {
Expand All @@ -228,35 +239,40 @@ private OneTimeToken getLastToken() {
@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
return http.build();
}

@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);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()

assertThat(getCurrentMinutes(token!!.expiresAt)).isEqualTo(10)
}
verify { resolver.resolve(any()) }
verify { tokenService.generate(expectedGenerateRequest) }
verify { tokenGenerationSuccessHandler.handle(any(), any(), eq(ott)) }

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 {
Expand Down Expand Up @@ -170,29 +180,41 @@ 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
return http.build()
}

@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()
}

}
Expand Down

0 comments on commit 10394c8

Please sign in to comment.