diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index ac72b75eb9..ec7da0f266 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.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. @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; @@ -53,6 +54,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; import org.springframework.security.authentication.ott.OneTimeToken; import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService; import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager; @@ -156,7 +158,9 @@ import org.springframework.security.web.server.authentication.logout.LogoutWebFi import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver; import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter; +import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver; import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter; import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler; import org.springframework.security.web.server.authorization.AuthorizationContext; @@ -5940,6 +5944,8 @@ public class ServerHttpSecurity { private ServerSecurityContextRepository securityContextRepository; + private ServerGenerateOneTimeTokenRequestResolver requestResolver; + private String loginProcessingUrl = "/login/ott"; private String defaultSubmitPageUrl = "/login/ott"; @@ -5985,6 +5991,7 @@ public class ServerHttpSecurity { getTokenGenerationSuccessHandler()); generateFilter .setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl)); + generateFilter.setGenerateRequestResolver(getRequestResolver()); http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN); } @@ -6112,6 +6119,32 @@ public class ServerHttpSecurity { return this; } + /** + * Use this {@link ServerGenerateOneTimeTokenRequestResolver} when resolving + * {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}. By default, + * the {@link DefaultServerGenerateOneTimeTokenRequestResolver} is used. + * @param requestResolver the + * {@link DefaultServerGenerateOneTimeTokenRequestResolver} to use + * @since 6.5 + */ + public OneTimeTokenLoginSpec generateRequestResolver( + ServerGenerateOneTimeTokenRequestResolver requestResolver) { + Assert.notNull(requestResolver, "generateRequestResolver cannot be null"); + this.requestResolver = requestResolver; + return this; + } + + private ServerGenerateOneTimeTokenRequestResolver getRequestResolver() { + if (this.requestResolver != null) { + return this.requestResolver; + } + ServerGenerateOneTimeTokenRequestResolver bean = getBeanOrNull( + ServerGenerateOneTimeTokenRequestResolver.class); + this.requestResolver = Objects.requireNonNullElseGet(bean, + DefaultServerGenerateOneTimeTokenRequestResolver::new); + return this.requestResolver; + } + /** * Specifies the URL to process the login request, defaults to {@code /login/ott}. * Only POST requests are processed, for that reason make sure that you pass a diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt index 3765a3e11a..05019e045c 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOneTimeTokenLoginDsl.kt @@ -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. @@ -18,6 +18,7 @@ package org.springframework.security.config.web.server import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService +import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver import org.springframework.security.web.server.authentication.ServerAuthenticationConverter import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler @@ -34,6 +35,7 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo * @property authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used + * @property generateRequestResolver the [ServerGenerateOneTimeTokenRequestResolver] to be used * @property defaultSubmitPageUrl sets the URL that the default submit page will be generated * @property showDefaultSubmitPage configures whether the default one-time token submit page should be shown * @property loginProcessingUrl the URL to process the login request @@ -50,6 +52,7 @@ class ServerOneTimeTokenLoginDsl { var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null var securityContextRepository: ServerSecurityContextRepository? = null + var generateRequestResolver: ServerGenerateOneTimeTokenRequestResolver? = null var defaultSubmitPageUrl: String? = null var loginProcessingUrl: String? = null var tokenGeneratingUrl: String? = null @@ -71,6 +74,7 @@ class ServerOneTimeTokenLoginDsl { ) } securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) } + generateRequestResolver?.also { oneTimeTokenLogin.generateRequestResolver(generateRequestResolver) } defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) } showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) } loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) } 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 c19f330eba..b83f46081e 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 @@ -271,10 +271,10 @@ public class OneTimeTokenLoginSpecTests { @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. """); 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 db4be9e313..674a1ec570 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 @@ -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. @@ -16,6 +16,7 @@ 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 @@ -26,6 +27,7 @@ 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 @@ -34,6 +36,8 @@ 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.ServerOneTimeTokenGenerationSuccessHandler @@ -43,6 +47,9 @@ 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 /** * Tests for [ServerOneTimeTokenLoginDsl] @@ -146,6 +153,48 @@ class ServerOneTimeTokenLoginDslTests { // @formatter:on } + @Test + fun `oneTimeToken when custom token expiration time set then authenticate`() { + spring.register(OneTimeTokenConfigWithCustomTokenExpirationTime::class.java).autowire() + + // @formatter:off + 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") + + 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 token = TestServerOneTimeTokenGenerationSuccessHandler.lastToken + + Assertions.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 + } + @Configuration @EnableWebFlux @EnableWebFluxSecurity @@ -199,6 +248,34 @@ class ServerOneTimeTokenLoginDslTests { MapReactiveUserDetailsService(User("user", "password", listOf())) } + @Configuration(proxyBeanMethods = false) + @EnableWebFlux + @EnableWebFluxSecurity + @Import(OneTimeTokenLoginSpecTests.UserDetailsServiceConfig::class) + open class OneTimeTokenConfigWithCustomTokenExpirationTime { + @Bean + open fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + // @formatter:off + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oneTimeTokenLogin { + tokenGenerationSuccessHandler = TestServerOneTimeTokenGenerationSuccessHandler() + } + } + } + + @Bean + open fun resolver(): ServerGenerateOneTimeTokenRequestResolver { + val resolver = DefaultServerGenerateOneTimeTokenRequestResolver() + return ServerGenerateOneTimeTokenRequestResolver { exchange -> + resolver.resolve(exchange) + .map { request -> GenerateOneTimeTokenRequest(request.username, Duration.ofSeconds(600)) } + } + } + } + private class TestServerOneTimeTokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler { private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null diff --git a/docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc b/docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc index f24ff5b87e..9d5412e4b1 100644 --- a/docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc +++ b/docs/modules/ROOT/pages/reactive/authentication/onetimetoken.adoc @@ -546,3 +546,35 @@ class MagicLinkOneTimeTokenGenerationSuccessHandler(val mailSender: MailSender): ---- ====== + +[[customize-generate-token-request]] +== Customize GenerateOneTimeTokenRequest Instance +There are a number of reasons that you may want to adjust an GenerateOneTimeTokenRequest. For example, you may want expiresIn to be set to 10 mins, which Spring Security sets to 5 mins by default. + +You can customize elements of GenerateOneTimeTokenRequest by publishing an ServerGenerateOneTimeTokenRequestResolver as a @Bean, like so: +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +ServerGenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() { + DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver(); + resolver.setExpiresIn(Duration.ofSeconds(600)); + return resolver; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun generateOneTimeTokenRequestResolver() : ServerGenerateOneTimeTokenRequestResolver { + return DefaultServerGenerateOneTimeTokenRequestResolver().apply { + this.setExpiresIn(Duration.ofMinutes(10)) + } +} +---- +====== diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java new file mode 100644 index 0000000000..f89298f6d4 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolver.java @@ -0,0 +1,62 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication.ott; + +import java.time.Duration; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Default implementation of {@link ServerGenerateOneTimeTokenRequestResolver}. Resolves + * {@link GenerateOneTimeTokenRequest} from username parameter. + * + * @author Max Batischev + * @since 6.5 + */ +public final class DefaultServerGenerateOneTimeTokenRequestResolver + implements ServerGenerateOneTimeTokenRequestResolver { + + private static final String USERNAME = "username"; + + private static final Duration DEFAULT_EXPIRES_IN = Duration.ofMinutes(5); + + private Duration expiresIn = DEFAULT_EXPIRES_IN; + + @Override + public Mono resolve(ServerWebExchange exchange) { + // @formatter:off + return exchange.getFormData() + .mapNotNull((data) -> data.getFirst(USERNAME)) + .switchIfEmpty(Mono.empty()) + .map((username) -> new GenerateOneTimeTokenRequest(username, this.expiresIn)); + // @formatter:on + } + + /** + * Sets one-time token expiration time + * @param expiresIn one-time token expiration time + */ + public void setExpiresIn(Duration expiresIn) { + Assert.notNull(expiresIn, "expiresIn cannot be null"); + this.expiresIn = expiresIn; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ott/GenerateOneTimeTokenWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/ott/GenerateOneTimeTokenWebFilter.java index 170d1d0b68..9a9640d358 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/ott/GenerateOneTimeTokenWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ott/GenerateOneTimeTokenWebFilter.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. @@ -19,7 +19,6 @@ package org.springframework.security.web.server.authentication.ott; import reactor.core.publisher.Mono; import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; @@ -37,12 +36,12 @@ import org.springframework.web.server.WebFilterChain; */ public final class GenerateOneTimeTokenWebFilter implements WebFilter { - private static final String USERNAME = "username"; - private final ReactiveOneTimeTokenService oneTimeTokenService; private ServerWebExchangeMatcher matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/ott/generate"); + private ServerGenerateOneTimeTokenRequestResolver generateRequestResolver = new DefaultServerGenerateOneTimeTokenRequestResolver(); + private final ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler; public GenerateOneTimeTokenWebFilter(ReactiveOneTimeTokenService oneTimeTokenService, @@ -58,10 +57,9 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter { // @formatter:off return this.matcher.matches(exchange) .filter(ServerWebExchangeMatcher.MatchResult::isMatch) - .then(exchange.getFormData()) - .mapNotNull((data) -> data.getFirst(USERNAME)) + .flatMap((result) -> this.generateRequestResolver.resolve(exchange)) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) - .flatMap((username) -> this.oneTimeTokenService.generate(new GenerateOneTimeTokenRequest(username))) + .flatMap(this.oneTimeTokenService::generate) .flatMap((token) -> this.oneTimeTokenGenerationSuccessHandler.handle(exchange, token)); // @formatter:on } @@ -75,4 +73,15 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter { this.matcher = matcher; } + /** + * Use the given {@link ServerGenerateOneTimeTokenRequestResolver} to resolve the + * request, defaults to {@link DefaultServerGenerateOneTimeTokenRequestResolver} + * @param requestResolver {@link ServerGenerateOneTimeTokenRequestResolver} + * @since 6.5 + */ + public void setGenerateRequestResolver(ServerGenerateOneTimeTokenRequestResolver requestResolver) { + Assert.notNull(requestResolver, "requestResolver cannot be null"); + this.generateRequestResolver = requestResolver; + } + } diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ott/ServerGenerateOneTimeTokenRequestResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ott/ServerGenerateOneTimeTokenRequestResolver.java new file mode 100644 index 0000000000..1f360813e1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ott/ServerGenerateOneTimeTokenRequestResolver.java @@ -0,0 +1,40 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication.ott; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; +import org.springframework.web.server.ServerWebExchange; + +/** + * A strategy for resolving a {@link GenerateOneTimeTokenRequest} from the + * {@link ServerWebExchange}. + * + * @author Max Batischev + * @since 6.5 + */ +public interface ServerGenerateOneTimeTokenRequestResolver { + + /** + * Resolves {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange} + * @param exchange {@link ServerWebExchange} to resolve + * @return {@link GenerateOneTimeTokenRequest} + */ + Mono resolve(ServerWebExchange exchange); + +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolverTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolverTests.java new file mode 100644 index 0000000000..c9bfc9eef1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/ott/DefaultServerGenerateOneTimeTokenRequestResolverTests.java @@ -0,0 +1,74 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authentication.ott; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultServerGenerateOneTimeTokenRequestResolver} + * + * @author Max Batischev + */ +public class DefaultServerGenerateOneTimeTokenRequestResolverTests { + + private final DefaultServerGenerateOneTimeTokenRequestResolver resolver = new DefaultServerGenerateOneTimeTokenRequestResolver(); + + @Test + void resolveWhenUsernameParameterIsPresentThenResolvesGenerateRequest() { + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/ott/generate") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body("username=user")); + + GenerateOneTimeTokenRequest request = this.resolver.resolve(exchange).block(); + + assertThat(request).isNotNull(); + assertThat(request.getUsername()).isEqualTo("user"); + assertThat(request.getExpiresIn()).isEqualTo(Duration.ofMinutes(5)); + } + + @Test + void resolveWhenUsernameParameterIsNotPresentThenNull() { + MockServerWebExchange exchange = MockServerWebExchange + .from(MockServerHttpRequest.post("/ott/generate").contentType(MediaType.APPLICATION_FORM_URLENCODED)); + + GenerateOneTimeTokenRequest request = this.resolver.resolve(exchange).block(); + + assertThat(request).isNull(); + } + + @Test + void resolveWhenExpiresInSetThenResolvesGenerateRequest() { + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/ott/generate") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body("username=user")); + this.resolver.setExpiresIn(Duration.ofSeconds(600)); + + GenerateOneTimeTokenRequest generateRequest = this.resolver.resolve(exchange).block(); + + assertThat(generateRequest.getExpiresIn()).isEqualTo(Duration.ofSeconds(600)); + } + +}