Use UserWebTestClientConfigurer

Closes gh-17496
This commit is contained in:
Rob Winch 2025-07-07 11:37:17 -05:00
parent dbb3b7e1f5
commit e48fdd5ed4
No known key found for this signature in database
7 changed files with 18 additions and 181 deletions

View File

@ -16,11 +16,6 @@
package org.springframework.security.config.web.server package org.springframework.security.config.web.server
import io.mockk.every
import io.mockk.mockk
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
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 org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -28,10 +23,6 @@ import org.springframework.context.ApplicationContext
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.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import org.springframework.http.client.reactive.ClientHttpConnector
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
import org.springframework.http.server.reactive.SslInfo
import org.springframework.lang.Nullable
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
import org.springframework.security.config.test.SpringTestContextExtension import org.springframework.security.config.test.SpringTestContextExtension
@ -41,19 +32,15 @@ import org.springframework.security.core.userdetails.User
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor
import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
import org.springframework.test.web.reactive.server.MockServerConfigurer import org.springframework.test.web.reactive.server.UserWebTestClientConfigurer.x509
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.WebTestClientConfigurer
import org.springframework.test.web.reactive.server.expectBody import org.springframework.test.web.reactive.server.expectBody
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.config.EnableWebFlux import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.server.ServerWebExchange import java.security.cert.Certificate
import org.springframework.web.server.ServerWebExchangeDecorator import java.security.cert.CertificateFactory
import org.springframework.web.server.WebFilter import java.security.cert.X509Certificate
import org.springframework.web.server.WebFilterChain
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import reactor.core.publisher.Mono
/** /**
* Tests for [ServerX509Dsl] * Tests for [ServerX509Dsl]
@ -83,7 +70,7 @@ class ServerX509DslTests {
val certificate = loadCert<X509Certificate>("rod.cer") val certificate = loadCert<X509Certificate>("rod.cer")
this.client this.client
.mutateWith(mockX509(certificate)) .mutateWith(x509(certificate))
.get() .get()
.uri("/username") .uri("/username")
.exchange() .exchange()
@ -111,7 +98,7 @@ class ServerX509DslTests {
val certificate = loadCert<X509Certificate>("rodatexampledotcom.cer") val certificate = loadCert<X509Certificate>("rodatexampledotcom.cer")
this.client this.client
.mutateWith(mockX509(certificate)) .mutateWith(x509(certificate))
.get() .get()
.uri("/username") .uri("/username")
.exchange() .exchange()
@ -143,7 +130,7 @@ class ServerX509DslTests {
val certificate = loadCert<X509Certificate>("rod.cer") val certificate = loadCert<X509Certificate>("rod.cer")
this.client this.client
.mutateWith(mockX509(certificate)) .mutateWith(x509(certificate))
.get() .get()
.uri("/username") .uri("/username")
.exchange() .exchange()
@ -195,43 +182,6 @@ class ServerX509DslTests {
} }
} }
private fun mockX509(certificate: X509Certificate): X509Mutator {
return X509Mutator(certificate)
}
private class X509Mutator internal constructor(private var certificate: X509Certificate) : WebTestClientConfigurer, MockServerConfigurer {
override fun afterConfigurerAdded(builder: WebTestClient.Builder,
@Nullable httpHandlerBuilder: WebHttpHandlerBuilder?,
@Nullable connector: ClientHttpConnector?) {
val filter = SetSslInfoWebFilter(certificate)
httpHandlerBuilder!!.filters { filters: MutableList<WebFilter> -> filters.add(0, filter) }
}
}
private class SetSslInfoWebFilter(var certificate: X509Certificate) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(decorate(exchange))
}
private fun decorate(exchange: ServerWebExchange): ServerWebExchange {
val decorated: ServerHttpRequestDecorator = object : ServerHttpRequestDecorator(exchange.request) {
override fun getSslInfo(): SslInfo {
val sslInfo: SslInfo = mockk()
every { sslInfo.sessionId } returns "sessionId"
every { sslInfo.peerCertificates } returns arrayOf(certificate)
return sslInfo
}
}
return object : ServerWebExchangeDecorator(exchange) {
override fun getRequest(): org.springframework.http.server.reactive.ServerHttpRequest {
return decorated
}
}
}
}
private fun <T : Certificate> loadCert(location: String): T { private fun <T : Certificate> loadCert(location: String): T {
ClassPathResource(location).inputStream.use { inputStream -> ClassPathResource(location).inputStream.use { inputStream ->
val certFactory = CertificateFactory.getInstance("X.509") val certFactory = CertificateFactory.getInstance("X.509")

View File

@ -176,6 +176,7 @@
**** xref:reactive/test/web/authentication.adoc[Testing Authentication] **** xref:reactive/test/web/authentication.adoc[Testing Authentication]
**** xref:reactive/test/web/csrf.adoc[Testing CSRF] **** xref:reactive/test/web/csrf.adoc[Testing CSRF]
**** xref:reactive/test/web/oauth2.adoc[Testing OAuth 2.0] **** xref:reactive/test/web/oauth2.adoc[Testing OAuth 2.0]
**** xref:reactive/test/web/x509.adoc[Testing X509]
** xref:reactive/configuration/webflux.adoc[WebFlux Security] ** xref:reactive/configuration/webflux.adoc[WebFlux Security]
* xref:native-image/index.adoc[GraalVM Native Image Support] * xref:native-image/index.adoc[GraalVM Native Image Support]
** xref:native-image/method-security.adoc[Method Security] ** xref:native-image/method-security.adoc[Method Security]

View File

@ -0,0 +1,4 @@
= X509
Spring Framework provides first class support for testing X509 with `WebTestClient`.
For details refer to javadoc:{spring-framework-api-url}org.springframework.test.web.reactive.server.UserWebTestClientConfigurer[].

View File

@ -21,31 +21,20 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.SslInfo;
import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509; import static org.springframework.test.web.reactive.server.UserWebTestClientConfigurer.x509;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/** /**
* Tests {@link CustomX509Configuration}. * Tests {@link CustomX509Configuration}.
@ -96,46 +85,6 @@ public class X509ConfigurationTests {
// @formatter:on // @formatter:on
} }
private static @NotNull WebTestClientConfigurer x509(X509Certificate certificate) {
return (builder, httpHandlerBuilder, connector) -> {
builder.apply(new WebTestClientConfigurer() {
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector) {
SslInfo sslInfo = new SslInfo() {
@Override
public @Nullable String getSessionId() {
return "sessionId";
}
@Override
public X509Certificate @Nullable [] getPeerCertificates() {
return new X509Certificate[] { certificate };
}
};
httpHandlerBuilder.filters((filters) -> filters.add(0, new SslInfoOverrideWebFilter(sslInfo)));
}
});
};
}
private static class SslInfoOverrideWebFilter implements WebFilter {
private final SslInfo sslInfo;
private SslInfoOverrideWebFilter(SslInfo sslInfo) {
this.sslInfo = sslInfo;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
.build();
ServerWebExchange sslInfoExchange = exchange.mutate().request(sslInfoRequest).build();
return chain.filter(sslInfoExchange);
}
}
private <T extends Certificate> T loadCert(String location) { private <T extends Certificate> T loadCert(String location) {
try (InputStream is = new ClassPathResource(location).getInputStream()) { try (InputStream is = new ClassPathResource(location).getInputStream()) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509");

View File

@ -26,6 +26,7 @@ import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
import org.springframework.test.web.reactive.server.UserWebTestClientConfigurer.x509
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.WebTestClientConfigurer import org.springframework.test.web.reactive.server.WebTestClientConfigurer
import org.springframework.util.Assert import org.springframework.util.Assert
@ -87,15 +88,6 @@ class X509ConfigurationTests {
// @formatter:on // @formatter:on
} }
private class SslInfoOverrideWebFilter(private val sslInfo: SslInfo) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
.build()
val sslInfoExchange = exchange.mutate().request(sslInfoRequest).build()
return chain.filter(sslInfoExchange)
}
}
private fun <T : Certificate?> loadCert(location: String): T { private fun <T : Certificate?> loadCert(location: String): T {
try { try {
ClassPathResource(location).getInputStream().use { `is` -> ClassPathResource(location).getInputStream().use { `is` ->
@ -106,28 +98,4 @@ class X509ConfigurationTests {
throw IllegalArgumentException(ex) throw IllegalArgumentException(ex)
} }
} }
companion object {
private fun x509(certificate: X509Certificate): WebTestClientConfigurer {
return WebTestClientConfigurer { builder: WebTestClient.Builder, httpHandlerBuilder: WebHttpHandlerBuilder?, connector: ClientHttpConnector? ->
val sslInfo: SslInfo = object : SslInfo {
override fun getSessionId(): String {
return "sessionId"
}
override fun getPeerCertificates(): Array<X509Certificate> {
return arrayOf(certificate)
}
}
Assert.notNull(httpHandlerBuilder, "httpHandlerBuilder should not be null")
httpHandlerBuilder!!.filters(Consumer { filters: MutableList<WebFilter> ->
filters.add(
0,
SslInfoOverrideWebFilter(sslInfo)
)
})
}
}
}
} }

View File

@ -69,31 +69,11 @@ public class ServerX509AuthenticationConverterTests {
@Test @Test
public void shouldReturnAuthenticationForValidCertificate() { public void shouldReturnAuthenticationForValidCertificate() {
givenExtractPrincipalWillReturn(); givenExtractPrincipalWillReturn();
this.request.sslInfo(new MockSslInfo(this.certificate)); this.request.sslInfo(SslInfo.from("123", this.certificate));
Authentication authentication = this.converter.convert(MockServerWebExchange.from(this.request.build())) Authentication authentication = this.converter.convert(MockServerWebExchange.from(this.request.build()))
.block(); .block();
assertThat(authentication.getName()).isEqualTo("Luke Taylor"); assertThat(authentication.getName()).isEqualTo("Luke Taylor");
assertThat(authentication.getCredentials()).isEqualTo(this.certificate); assertThat(authentication.getCredentials()).isEqualTo(this.certificate);
} }
class MockSslInfo implements SslInfo {
private final X509Certificate[] peerCertificates;
MockSslInfo(X509Certificate... peerCertificates) {
this.peerCertificates = peerCertificates;
}
@Override
public String getSessionId() {
return "mock-session-id";
}
@Override
public X509Certificate[] getPeerCertificates() {
return this.peerCertificates;
}
}
} }

View File

@ -16,7 +16,6 @@
package org.springframework.security.web.server.csrf; package org.springframework.security.web.server.csrf;
import java.security.cert.X509Certificate;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
@ -179,7 +178,7 @@ class CookieServerCsrfTokenRepositoryTests {
@Test @Test
void saveTokenWhenSslInfoPresentThenSecure() { void saveTokenWhenSslInfoPresentThenSecure() {
this.request.sslInfo(new MockSslInfo()); this.request.sslInfo(SslInfo.from("sessionId"));
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
this.csrfTokenRepository.saveToken(exchange, createToken()).block(); this.csrfTokenRepository.saveToken(exchange, createToken()).block();
ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
@ -239,7 +238,7 @@ class CookieServerCsrfTokenRepositoryTests {
@Test @Test
void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() { void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecure() {
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
this.request.sslInfo(new MockSslInfo()); this.request.sslInfo(SslInfo.from("sessionId"));
this.csrfTokenRepository.setSecure(false); this.csrfTokenRepository.setSecure(false);
this.csrfTokenRepository.saveToken(exchange, createToken()).block(); this.csrfTokenRepository.saveToken(exchange, createToken()).block();
ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
@ -250,7 +249,7 @@ class CookieServerCsrfTokenRepositoryTests {
@Test @Test
void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecureUsingCustomizer() { void saveTokenWhenSecureFlagFalseAndSslInfoThenNotSecureUsingCustomizer() {
MockServerWebExchange exchange = MockServerWebExchange.from(this.request); MockServerWebExchange exchange = MockServerWebExchange.from(this.request);
this.request.sslInfo(new MockSslInfo()); this.request.sslInfo(SslInfo.from("sessionId"));
this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.secure(false)); this.csrfTokenRepository.setCookieCustomizer((customizer) -> customizer.secure(false));
this.csrfTokenRepository.saveToken(exchange, createToken()).block(); this.csrfTokenRepository.saveToken(exchange, createToken()).block();
ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName); ResponseCookie cookie = exchange.getResponse().getCookies().getFirst(this.expectedCookieName);
@ -401,18 +400,4 @@ class CookieServerCsrfTokenRepositoryTests {
return new DefaultCsrfToken(headerName, parameterName, tokenValue); return new DefaultCsrfToken(headerName, parameterName, tokenValue);
} }
static class MockSslInfo implements SslInfo {
@Override
public String getSessionId() {
return "sessionId";
}
@Override
public X509Certificate[] getPeerCertificates() {
return new X509Certificate[] {};
}
}
} }