mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-27 14:22:47 +00:00
Add Support ServerGenerateOneTimeTokenRequestResolver
Closes gh-16488 Signed-off-by: Max Batischev <mblancer@mail.ru>
This commit is contained in:
parent
981e3fd779
commit
be81377235
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
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.DelegatingReactiveAuthenticationManager;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
|
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.OneTimeToken;
|
||||||
import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
|
import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
|
||||||
import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager;
|
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.SecurityContextServerLogoutHandler;
|
||||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
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.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.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.ServerOneTimeTokenAuthenticationConverter;
|
||||||
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
|
||||||
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
||||||
@ -5940,6 +5944,8 @@ public class ServerHttpSecurity {
|
|||||||
|
|
||||||
private ServerSecurityContextRepository securityContextRepository;
|
private ServerSecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
|
private ServerGenerateOneTimeTokenRequestResolver requestResolver;
|
||||||
|
|
||||||
private String loginProcessingUrl = "/login/ott";
|
private String loginProcessingUrl = "/login/ott";
|
||||||
|
|
||||||
private String defaultSubmitPageUrl = "/login/ott";
|
private String defaultSubmitPageUrl = "/login/ott";
|
||||||
@ -5985,6 +5991,7 @@ public class ServerHttpSecurity {
|
|||||||
getTokenGenerationSuccessHandler());
|
getTokenGenerationSuccessHandler());
|
||||||
generateFilter
|
generateFilter
|
||||||
.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
|
.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
|
||||||
|
generateFilter.setGenerateRequestResolver(getRequestResolver());
|
||||||
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
|
http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6112,6 +6119,32 @@ public class ServerHttpSecurity {
|
|||||||
return this;
|
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}.
|
* 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
|
* Only POST requests are processed, for that reason make sure that you pass a
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ReactiveAuthenticationManager
|
||||||
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService
|
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.ServerAuthenticationConverter
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
|
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 authenticationConverter Use this [ServerAuthenticationConverter] when converting incoming requests to an authentication
|
||||||
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
|
* @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] to use when authentication
|
||||||
* @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] to be used
|
* @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 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 showDefaultSubmitPage configures whether the default one-time token submit page should be shown
|
||||||
* @property loginProcessingUrl the URL to process the login request
|
* @property loginProcessingUrl the URL to process the login request
|
||||||
@ -50,6 +52,7 @@ class ServerOneTimeTokenLoginDsl {
|
|||||||
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
|
var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null
|
||||||
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
|
var tokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler? = null
|
||||||
var securityContextRepository: ServerSecurityContextRepository? = null
|
var securityContextRepository: ServerSecurityContextRepository? = null
|
||||||
|
var generateRequestResolver: ServerGenerateOneTimeTokenRequestResolver? = null
|
||||||
var defaultSubmitPageUrl: String? = null
|
var defaultSubmitPageUrl: String? = null
|
||||||
var loginProcessingUrl: String? = null
|
var loginProcessingUrl: String? = null
|
||||||
var tokenGeneratingUrl: String? = null
|
var tokenGeneratingUrl: String? = null
|
||||||
@ -71,6 +74,7 @@ class ServerOneTimeTokenLoginDsl {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
|
securityContextRepository?.also { oneTimeTokenLogin.securityContextRepository(securityContextRepository) }
|
||||||
|
generateRequestResolver?.also { oneTimeTokenLogin.generateRequestResolver(generateRequestResolver) }
|
||||||
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
|
defaultSubmitPageUrl?.also { oneTimeTokenLogin.defaultSubmitPageUrl(defaultSubmitPageUrl) }
|
||||||
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
|
showDefaultSubmitPage?.also { oneTimeTokenLogin.showDefaultSubmitPage(showDefaultSubmitPage!!) }
|
||||||
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }
|
loginProcessingUrl?.also { oneTimeTokenLogin.loginProcessingUrl(loginProcessingUrl) }
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
package org.springframework.security.config.web.server
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import reactor.core.publisher.Mono
|
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.annotation.Import
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
|
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.config.annotation.web.reactive.EnableWebFluxSecurity
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
import org.springframework.security.config.test.SpringTestContext
|
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.ReactiveUserDetailsService
|
||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.core.userdetails.User
|
||||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
|
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.SecurityWebFilterChain
|
||||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
|
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler
|
||||||
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler
|
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.reactive.function.BodyInserters
|
||||||
import org.springframework.web.server.ServerWebExchange
|
import org.springframework.web.server.ServerWebExchange
|
||||||
import org.springframework.web.util.UriBuilder
|
import org.springframework.web.util.UriBuilder
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for [ServerOneTimeTokenLoginDsl]
|
* Tests for [ServerOneTimeTokenLoginDsl]
|
||||||
@ -146,6 +153,48 @@ class ServerOneTimeTokenLoginDslTests {
|
|||||||
// @formatter:on
|
// @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
|
@Configuration
|
||||||
@EnableWebFlux
|
@EnableWebFlux
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@ -199,6 +248,34 @@ class ServerOneTimeTokenLoginDslTests {
|
|||||||
MapReactiveUserDetailsService(User("user", "password", listOf()))
|
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 class TestServerOneTimeTokenGenerationSuccessHandler: ServerOneTimeTokenGenerationSuccessHandler {
|
||||||
private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null
|
private var delegate: ServerRedirectOneTimeTokenGenerationSuccessHandler? = null
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
======
|
||||||
|
@ -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<GenerateOneTimeTokenRequest> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
|
|
||||||
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
|
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.ServerWebExchangeMatcher;
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
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 {
|
public final class GenerateOneTimeTokenWebFilter implements WebFilter {
|
||||||
|
|
||||||
private static final String USERNAME = "username";
|
|
||||||
|
|
||||||
private final ReactiveOneTimeTokenService oneTimeTokenService;
|
private final ReactiveOneTimeTokenService oneTimeTokenService;
|
||||||
|
|
||||||
private ServerWebExchangeMatcher matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/ott/generate");
|
private ServerWebExchangeMatcher matcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/ott/generate");
|
||||||
|
|
||||||
|
private ServerGenerateOneTimeTokenRequestResolver generateRequestResolver = new DefaultServerGenerateOneTimeTokenRequestResolver();
|
||||||
|
|
||||||
private final ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
|
private final ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler;
|
||||||
|
|
||||||
public GenerateOneTimeTokenWebFilter(ReactiveOneTimeTokenService oneTimeTokenService,
|
public GenerateOneTimeTokenWebFilter(ReactiveOneTimeTokenService oneTimeTokenService,
|
||||||
@ -58,10 +57,9 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter {
|
|||||||
// @formatter:off
|
// @formatter:off
|
||||||
return this.matcher.matches(exchange)
|
return this.matcher.matches(exchange)
|
||||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||||
.then(exchange.getFormData())
|
.flatMap((result) -> this.generateRequestResolver.resolve(exchange))
|
||||||
.mapNotNull((data) -> data.getFirst(USERNAME))
|
|
||||||
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
|
.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));
|
.flatMap((token) -> this.oneTimeTokenGenerationSuccessHandler.handle(exchange, token));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
@ -75,4 +73,15 @@ public final class GenerateOneTimeTokenWebFilter implements WebFilter {
|
|||||||
this.matcher = matcher;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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<GenerateOneTimeTokenRequest> resolve(ServerWebExchange exchange);
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user