diff --git a/docs/antora.yml b/docs/antora.yml
index 02262db72e..c6a145aaab 100644
--- a/docs/antora.yml
+++ b/docs/antora.yml
@@ -18,3 +18,4 @@ asciidoc:
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
include-java: 'example$docs-src/test/java/org/springframework/security/docs'
include-kotlin: 'example$docs-src/test/kotlin/org/springframework/security/kt/docs'
+ include-xml: 'example$docs-src/test/resources/org/springframework/security/docs'
diff --git a/docs/modules/ROOT/pages/reactive/authentication/x509.adoc b/docs/modules/ROOT/pages/reactive/authentication/x509.adoc
index a077dd5c65..947084423c 100644
--- a/docs/modules/ROOT/pages/reactive/authentication/x509.adoc
+++ b/docs/modules/ROOT/pages/reactive/authentication/x509.adoc
@@ -5,98 +5,16 @@ Similar to xref:servlet/authentication/x509.adoc#servlet-x509[Servlet X.509 auth
The following example shows a reactive x509 security configuration:
-[tabs]
-======
-Java::
-+
-[source,java,role="primary"]
-----
-@Bean
-public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
- http
- .x509(withDefaults())
- .authorizeExchange(exchanges -> exchanges
- .anyExchange().permitAll()
- );
- return http.build();
-}
-----
+include-code::./DefaultX509Configuration[tag=springSecurity,indent=0]
-Kotlin::
-+
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
- return http {
- x509 { }
- authorizeExchange {
- authorize(anyExchange, authenticated)
- }
- }
-}
-----
-======
-
-In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used. The default principal extractor is `SubjectDnX509PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
+In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used.
+The default principal extractor is `SubjectX500PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client.
+The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
The following example demonstrates how these defaults can be overridden:
-[tabs]
-======
-Java::
-+
-[source,java,role="primary"]
-----
-@Bean
-public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
- SubjectDnX509PrincipalExtractor principalExtractor =
- new SubjectDnX509PrincipalExtractor();
+include-code::./CustomX509Configuration[tag=springSecurity,indent=0]
- principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)");
-
- ReactiveAuthenticationManager authenticationManager = authentication -> {
- authentication.setAuthenticated("Trusted Org Unit".equals(authentication.getName()));
- return Mono.just(authentication);
- };
-
- http
- .x509(x509 -> x509
- .principalExtractor(principalExtractor)
- .authenticationManager(authenticationManager)
- )
- .authorizeExchange(exchanges -> exchanges
- .anyExchange().authenticated()
- );
- return http.build();
-}
-----
-
-Kotlin::
-+
-[source,kotlin,role="secondary"]
-----
-@Bean
-fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
- val customPrincipalExtractor = SubjectDnX509PrincipalExtractor()
- customPrincipalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)")
- val customAuthenticationManager = ReactiveAuthenticationManager { authentication: Authentication ->
- authentication.isAuthenticated = "Trusted Org Unit" == authentication.name
- Mono.just(authentication)
- }
- return http {
- x509 {
- principalExtractor = customPrincipalExtractor
- authenticationManager = customAuthenticationManager
- }
- authorizeExchange {
- authorize(anyExchange, authenticated)
- }
- }
-}
-----
-======
-
-In the previous example, a username is extracted from the OU field of a client certificate instead of CN, and account lookup using `ReactiveUserDetailsService` is not performed at all. Instead, if the provided certificate issued to an OU named "`Trusted Org Unit`", a request is authenticated.
+In the previous example, a username is extracted from the `emailAddress` field of a client certificate instead of CN, and account lookup uses a custom `ReactiveAuthenticationManager` instance.
For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, see https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509.
diff --git a/docs/modules/ROOT/pages/servlet/authentication/x509.adoc b/docs/modules/ROOT/pages/servlet/authentication/x509.adoc
index 26dbb1a900..2d670a632e 100644
--- a/docs/modules/ROOT/pages/servlet/authentication/x509.adoc
+++ b/docs/modules/ROOT/pages/servlet/authentication/x509.adoc
@@ -14,37 +14,27 @@ You should get this working before trying it out with Spring Security.
The Spring Security X.509 module extracts the certificate by using a filter.
It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure.
-
+[[servlet-x509-config]]
== Adding X.509 Authentication to Your Web Application
-Enabling X.509 client authentication is very straightforward.
-To do so, add the `` element to your http security namespace configuration:
-[source,xml]
-----
-
-...
- ;
-
-----
+Similar to xref:reactive/authentication/x509.adoc[Reactive X.509 authentication], the servlet x509 authentication filter allows extracting an authentication token from a certificate provided by a client.
-The element has two optional attributes:
+The following example shows a reactive x509 security configuration:
-* `subject-principal-regex`.
-The regular expression used to extract a username from the certificate's subject name.
-The default value is shown in the preceding listing.
-This is the username that is passed to the `UserDetailsService` to load the authorities for the user.
-* `user-service-ref`.
-This is the bean ID of the `UserDetailsService` to be used with X.509.
-It is not needed if there is only one defined in your application context.
+include-code::./DefaultX509Configuration[tag=springSecurity,indent=0]
+
+In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used.
+The default principal extractor is `SubjectX500PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client.
+The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired.
+
+The following example demonstrates how these defaults can be overridden:
+
+include-code::./CustomX509Configuration[tag=springSecurity,indent=0]
+
+In the previous example, a username is extracted from the `emailAddress` field of a client certificate instead of CN, and account lookup uses a custom `ReactiveAuthenticationManager` instance.
+
+For an example of configuring Netty and `WebClient` or `curl` command-line tool to use mutual TLS and enable X.509 authentication, see https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509.
-The `subject-principal-regex` should contain a single group.
-For example, the default expression (`CN=(.*?)`) matches the common name field.
-So, if the subject name in the certificate is "CN=Jimi Hendrix, OU=...", this gives a user name of "Jimi Hendrix".
-The matches are case insensitive.
-So "emailAddress=(+.*?+)," matches "EMAILADDRESS=jimi@hendrix.org,CN=...", giving a user name "jimi@hendrix.org".
-If the client presents a certificate and a valid username is successfully extracted, there should be a valid `Authentication` object in the security context.
-If no certificate is found or no corresponding user could be found, the security context remains empty.
-This means that you can use X.509 authentication with other options, such as a form-based login.
[[x509-ssl-config]]
== Setting up SSL in Tomcat
diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java
new file mode 100644
index 0000000000..8f7f8c8278
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.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.docs.reactive.authentication.reactivex509;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
+import org.springframework.web.reactive.config.EnableWebFlux;
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableWebFluxSecurity
+@EnableWebFlux
+public class CustomX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+ SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
+ principalExtractor.setExtractPrincipalNameFromEmail(true);
+
+ // @formatter:off
+ UserDetails user = User
+ .withUsername("luke@monkeymachine")
+ .password("password")
+ .roles("USER")
+ .build();
+ // @formatter:on
+
+ ReactiveUserDetailsService users = new MapReactiveUserDetailsService(user);
+ ReactiveAuthenticationManager authenticationManager = new ReactivePreAuthenticatedAuthenticationManager(users);
+
+ // @formatter:off
+ http
+ .x509(x509 -> x509
+ .principalExtractor(principalExtractor)
+ .authenticationManager(authenticationManager)
+ )
+ .authorizeExchange(exchanges -> exchanges
+ .anyExchange().authenticated()
+ );
+ // @formatter:on
+ return http.build();
+ }
+ // end::springSecurity[]
+
+}
diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java
new file mode 100644
index 0000000000..0fd17fa1d8
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java
@@ -0,0 +1,67 @@
+/*
+ * 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.docs.reactive.authentication.reactivex509;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.web.reactive.config.EnableWebFlux;
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableWebFluxSecurity
+@EnableWebFlux
+public class DefaultX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+ // @formatter:off
+ http
+ .x509(Customizer.withDefaults())
+ .authorizeExchange(exchanges -> exchanges
+ .anyExchange().authenticated()
+ );
+ // @formatter:on
+ return http.build();
+ }
+ // end::springSecurity[]
+
+ @Bean
+ ReactiveUserDetailsService userDetailsService() {
+ // @formatter:off
+ UserDetails user = User
+ .withUsername("rod")
+ .password("password")
+ .roles("USER")
+ .build();
+ // @formatter:on
+
+ return new MapReactiveUserDetailsService(user);
+ }
+}
diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java
new file mode 100644
index 0000000000..475787da4f
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java
@@ -0,0 +1,148 @@
+/*
+ * 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.docs.reactive.authentication.reactivex509;
+
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+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.extension.ExtendWith;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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.SpringTestContextExtension;
+import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
+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.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.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+
+/**
+ * Tests {@link CustomX509Configuration}.
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class X509ConfigurationTests {
+
+ public final SpringTestContext spring = new SpringTestContext(this);
+
+ WebTestClient client;
+
+ @Autowired
+ void setSpringSecurityFilterChain(WebFilter springSecurityFilterChain) {
+ this.client = WebTestClient.bindToController(WebTestClientBuilder.Http200RestController.class)
+ .webFilter(springSecurityFilterChain)
+ .apply(springSecurity())
+ .configureClient()
+ .build();
+ }
+
+ @Test
+ void x509WhenDefaultX509Configuration() throws Exception {
+ this.spring.register(DefaultX509Configuration.class).autowire();
+ X509Certificate certificate = loadCert("rod.cer");
+ // @formatter:off
+ this.client
+ .mutateWith(x509(certificate))
+ .get()
+ .uri("/")
+ .exchange()
+ .expectStatus().isOk();
+ // @formatter:on
+ }
+
+ @Test
+ void x509WhenCustomX509Configuration() throws Exception {
+ this.spring.register(CustomX509Configuration.class).autowire();
+ X509Certificate certificate = X509TestUtils.buildTestCertificate();
+ // @formatter:off
+ this.client
+ .mutateWith(x509(certificate))
+ .get()
+ .uri("/")
+ .exchange()
+ .expectStatus().isOk();
+ // @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 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 loadCert(String location) {
+ try (InputStream is = new ClassPathResource(location).getInputStream()) {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ return (T) certFactory.generateCertificate(is);
+ }
+ catch (Exception ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+}
diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java
new file mode 100644
index 0000000000..70b46b7c54
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java
@@ -0,0 +1,75 @@
+/*
+ * 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.docs.servlet.authentication.servletx509config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+public class CustomX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+ SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
+ principalExtractor.setExtractPrincipalNameFromEmail(true);
+
+
+ // @formatter:off
+ http
+ .x509((x509) -> x509
+ .x509PrincipalExtractor(principalExtractor)
+ )
+ .authorizeHttpRequests((exchanges) -> exchanges
+ .anyRequest().authenticated()
+ );
+ // @formatter:on
+ return http.build();
+ }
+ // end::springSecurity[]
+
+ @Bean
+ UserDetailsService userDetailsService() {
+ // @formatter:off
+ UserDetails user = User
+ .withUsername("luke@monkeymachine")
+ .password("password")
+ .roles("USER")
+ .build();
+ // @formatter:on
+
+ return new InMemoryUserDetailsManager(user);
+ }
+}
diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java
new file mode 100644
index 0000000000..a347235386
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java
@@ -0,0 +1,67 @@
+/*
+ * 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.docs.servlet.authentication.servletx509config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+public class DefaultX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .x509(Customizer.withDefaults())
+ .authorizeHttpRequests(exchanges -> exchanges
+ .anyRequest().authenticated()
+ );
+ // @formatter:on
+ return http.build();
+ }
+ // end::springSecurity[]
+
+ @Bean
+ UserDetailsService userDetailsService() {
+ // @formatter:off
+ UserDetails user = User
+ .withUsername("rod")
+ .password("password")
+ .roles("USER")
+ .build();
+ // @formatter:on
+
+ return new InMemoryUserDetailsManager(user);
+ }
+}
diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java
new file mode 100644
index 0000000000..c6aa6f6d41
--- /dev/null
+++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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.docs.servlet.authentication.servletx509config;
+
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+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.extension.ExtendWith;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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.SpringTestContextExtension;
+import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+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.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests {@link CustomX509Configuration}.
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class X509ConfigurationTests {
+
+ public final SpringTestContext spring = new SpringTestContext(this);
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Test
+ void x509WhenDefaultX509Configuration() throws Exception {
+ this.spring.register(DefaultX509Configuration.class, Http200Controller.class).autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/").with(x509("rod.cer")))
+ .andExpect(status().isOk())
+ .andExpect(authenticated().withUsername("rod"));
+ // @formatter:on
+ }
+
+ @Test
+ void x509WhenDefaultX509ConfigurationXml() throws Exception {
+ this.spring.testConfigLocations("DefaultX509Configuration.xml").autowire();
+ // @formatter:off
+ this.mockMvc.perform(get("/").with(x509("rod.cer")))
+ .andExpect(authenticated().withUsername("rod"));
+ // @formatter:on
+ }
+
+ @Test
+ void x509WhenCustomX509Configuration() throws Exception {
+ this.spring.register(CustomX509Configuration.class, Http200Controller.class).autowire();
+ X509Certificate certificate = X509TestUtils.buildTestCertificate();
+ // @formatter:off
+ this.mockMvc.perform(get("/").with(x509(certificate)))
+ .andExpect(status().isOk())
+ .andExpect(authenticated().withUsername("luke@monkeymachine"));
+ // @formatter:on
+ }
+
+ @RestController
+ static class Http200Controller {
+ @GetMapping("/**")
+ String ok() {
+ return "ok";
+ }
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt
new file mode 100644
index 0000000000..78c4fbe34b
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt
@@ -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.kt.docs.reactive.authentication.reactivex509
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.ReactiveAuthenticationManager
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity.http
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebFlux
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class CustomX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ val extractor = SubjectX500PrincipalExtractor()
+ extractor.setExtractPrincipalNameFromEmail(true)
+
+ // @formatter:off
+ val user = User
+ .withUsername("luke@monkeymachine")
+ .password("password")
+ .roles("USER")
+ .build()
+ // @formatter:on
+
+ val users: ReactiveUserDetailsService = MapReactiveUserDetailsService(user)
+ val authentication: ReactiveAuthenticationManager = ReactivePreAuthenticatedAuthenticationManager(users)
+
+ return http {
+ x509 {
+ principalExtractor = extractor
+ authenticationManager = authentication
+ }
+ authorizeExchange {
+ authorize(anyExchange, authenticated)
+ }
+ }
+ }
+ // end::springSecurity[]
+
+
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt
new file mode 100644
index 0000000000..61e5d65db5
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.kt.docs.reactive.authentication.reactivex509
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.web.server.ServerHttpSecurity
+import org.springframework.security.config.web.server.invoke
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebFlux
+@EnableWebFluxSecurity
+@Configuration(proxyBeanMethods = false)
+class DefaultX509Configuration {
+
+ // tag::springSecurity[]
+ @Bean
+ fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+ return http {
+ x509 { }
+ authorizeExchange {
+ authorize(anyExchange, authenticated)
+ }
+ }
+ }
+ // end::springSecurity[]
+
+ @Bean
+ fun userDetailsService(): MapReactiveUserDetailsService {
+ // @formatter:off
+ val user = User
+ .withUsername("rod")
+ .password("password")
+ .roles("USER")
+ .build()
+ // @formatter:on
+
+ return MapReactiveUserDetailsService(user)
+ }
+
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt
new file mode 100644
index 0000000000..4802660f06
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.kt.docs.reactive.authentication.reactivex509
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.core.io.ClassPathResource
+import org.springframework.http.client.reactive.ClientHttpConnector
+import org.springframework.http.server.reactive.SslInfo
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
+import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController
+import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
+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.WebFilterChain
+import org.springframework.web.server.adapter.WebHttpHandlerBuilder
+import reactor.core.publisher.Mono
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.util.function.Consumer
+
+/**
+ * Tests [CustomX509Configuration].
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class X509ConfigurationTests {
+ @JvmField
+ val spring: SpringTestContext = SpringTestContext(this)
+
+ var client: WebTestClient? = null
+
+ @Autowired
+ fun setSpringSecurityFilterChain(springSecurityFilterChain: WebFilter) {
+ this.client = WebTestClient
+ .bindToController(Http200RestController::class.java)
+ .webFilter(springSecurityFilterChain)
+ .apply(SecurityMockServerConfigurers.springSecurity())
+ .configureClient()
+ .build()
+ }
+
+ @Test
+ fun x509WhenDefaultX509Configuration() {
+ this.spring.register(DefaultX509Configuration::class.java).autowire()
+ val certificate = loadCert("rod.cer")
+ // @formatter:off
+ this.client!!.mutateWith(x509(certificate))
+ .get()
+ .uri("/")
+ .exchange()
+ .expectStatus().isOk()
+ // @formatter:on
+ }
+
+ @Test
+ fun x509WhenCustomX509Configuration() {
+ this.spring.register(CustomX509Configuration::class.java).autowire()
+ val certificate = X509TestUtils.buildTestCertificate()
+ // @formatter:off
+ this.client!!.mutateWith(x509(certificate))
+ .get()
+ .uri("/")
+ .exchange()
+ .expectStatus().isOk()
+ // @formatter:on
+ }
+
+ private class SslInfoOverrideWebFilter(private val sslInfo: SslInfo) : WebFilter {
+ override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono {
+ val sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
+ .build()
+ val sslInfoExchange = exchange.mutate().request(sslInfoRequest).build()
+ return chain.filter(sslInfoExchange)
+ }
+ }
+
+ private fun loadCert(location: String): T {
+ try {
+ ClassPathResource(location).getInputStream().use { `is` ->
+ val certFactory = CertificateFactory.getInstance("X.509")
+ return certFactory.generateCertificate(`is`) as T
+ }
+ } catch (ex: Exception) {
+ 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 {
+ return arrayOf(certificate)
+ }
+ }
+ httpHandlerBuilder.filters(Consumer { filters: MutableList ->
+ filters.add(
+ 0,
+ SslInfoOverrideWebFilter(sslInfo)
+ )
+ })
+ }
+ }
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt
new file mode 100644
index 0000000000..b079ce26fb
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.kt.docs.servlet.authentication.servlet509config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.provisioning.InMemoryUserDetailsManager
+import org.springframework.security.web.DefaultSecurityFilterChain
+import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class CustomX509Configuration {
+ // tag::springSecurity[]
+ @Bean
+ fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
+ val principalExtractor = SubjectX500PrincipalExtractor()
+ principalExtractor.setExtractPrincipalNameFromEmail(true)
+
+ // @formatter:off
+ http {
+ authorizeHttpRequests {
+ authorize(anyRequest, authenticated)
+ }
+ x509 {
+ x509PrincipalExtractor = principalExtractor
+ }
+ }
+ return http.build()
+ }
+ // end::springSecurity[]
+
+ @Bean
+ fun userDetailsService(): UserDetailsService {
+ // @formatter:off
+ val user = User
+ .withUsername("luke@monkeymachine")
+ .password("password")
+ .roles("USER")
+ .build()
+ // @formatter:on
+ return InMemoryUserDetailsManager(user)
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt
new file mode 100644
index 0000000000..3c777ebe32
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.kt.docs.servlet.authentication.servlet509config
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.invoke
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.provisioning.InMemoryUserDetailsManager
+import org.springframework.security.web.DefaultSecurityFilterChain
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+
+/**
+ * Demonstrates custom configuration for x509 reactive configuration.
+ *
+ * @author Rob Winch
+ */
+@EnableWebMvc
+@EnableWebSecurity
+@Configuration(proxyBeanMethods = false)
+class DefaultX509Configuration {
+ // tag::springSecurity[]
+ @Bean
+ fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
+ // @formatter:off
+ http {
+ authorizeHttpRequests {
+ authorize(anyRequest, authenticated)
+ }
+ x509 { }
+ }
+ // @formatter:on
+ return http.build()
+ }
+ // end::springSecurity[]
+
+ @Bean
+ fun userDetailsService(): UserDetailsService {
+ // @formatter:off
+ val user = User
+ .withUsername("rod")
+ .password("password")
+ .roles("USER")
+ .build()
+ // @formatter:on
+ return InMemoryUserDetailsManager(user)
+ }
+}
diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt
new file mode 100644
index 0000000000..edaf503039
--- /dev/null
+++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.kt.docs.servlet.authentication.servlet509config
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
+import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
+import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Tests [CustomX509Configuration].
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class X509ConfigurationTests {
+ @JvmField
+ val spring: SpringTestContext = SpringTestContext(this)
+
+ @Autowired
+ var mockMvc: MockMvc? = null
+
+ @Test
+ @Throws(Exception::class)
+ fun x509WhenDefaultX509Configuration() {
+ this.spring.register(DefaultX509Configuration::class.java, Http200Controller::class.java).autowire()
+ // @formatter:off
+ this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
+ .andExpect(status().isOk())
+ .andExpect(authenticated().withUsername("rod"))
+ // @formatter:on
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun x509WhenCustomX509Configuration() {
+ this.spring.register(CustomX509Configuration::class.java, Http200Controller::class.java).autowire()
+ val certificate = X509TestUtils.buildTestCertificate()
+ // @formatter:off
+ this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509(certificate)))
+ .andExpect(status().isOk())
+ .andExpect(authenticated().withUsername("luke@monkeymachine"))
+ // @formatter:on
+ }
+
+ @RestController
+ internal class Http200Controller {
+ @GetMapping("/**")
+ fun ok(): String {
+ return "ok"
+ }
+ }
+}
diff --git a/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml
new file mode 100644
index 0000000000..cc2772e34b
--- /dev/null
+++ b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml
new file mode 100644
index 0000000000..62621d1cab
--- /dev/null
+++ b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+