Преглед на файлове

Update x509 Reference

- Use include-code
- Demo how to customize SubjectX500PrincipalExtractor
Rob Winch преди 2 месеца
родител
ревизия
e3add59550
променени са 17 файла, в които са добавени 1120 реда и са изтрити 116 реда
  1. 1 0
      docs/antora.yml
  2. 6 88
      docs/modules/ROOT/pages/reactive/authentication/x509.adoc
  3. 18 28
      docs/modules/ROOT/pages/servlet/authentication/x509.adoc
  4. 74 0
      docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java
  5. 67 0
      docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java
  6. 148 0
      docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java
  7. 75 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java
  8. 67 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java
  9. 103 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java
  10. 74 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt
  11. 64 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt
  12. 131 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt
  13. 69 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt
  14. 64 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt
  15. 75 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt
  16. 45 0
      docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml
  17. 39 0
      docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml

+ 1 - 0
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'

+ 6 - 88
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();
-
-	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)
-        }
-    }
-}
-----
-======
+include-code::./CustomX509Configuration[tag=springSecurity,indent=0]
 
-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.

+ 18 - 28
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 `<x509/>` element to your http security namespace configuration:
 
-[source,xml]
-----
-<http>
-...
-	<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
-</http>
-----
+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 following example shows a reactive x509 security configuration:
+
+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 element has two optional attributes:
-
-* `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.
-
-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

+ 74 - 0
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[]
+
+}

+ 67 - 0
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);
+	}
+}

+ 148 - 0
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<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) {
+		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);
+		}
+	}
+}

+ 75 - 0
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);
+	}
+}

+ 67 - 0
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);
+	}
+}

+ 103 - 0
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";
+		}
+	}
+}

+ 74 - 0
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[]
+
+
+}

+ 64 - 0
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)
+    }
+
+}

+ 131 - 0
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<WebTestClient.ControllerSpec>(springSecurityFilterChain)
+            .apply<WebTestClient.ControllerSpec>(SecurityMockServerConfigurers.springSecurity())
+            .configureClient()
+            .build()
+    }
+
+    @Test
+    fun x509WhenDefaultX509Configuration() {
+        this.spring.register(DefaultX509Configuration::class.java).autowire()
+        val certificate = loadCert<X509Certificate>("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<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 {
+        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<X509Certificate?> {
+                        return arrayOf(certificate)
+                    }
+                }
+                httpHandlerBuilder.filters(Consumer { filters: MutableList<WebFilter> ->
+                    filters.add(
+                        0,
+                        SslInfoOverrideWebFilter(sslInfo)
+                    )
+                })
+            }
+        }
+    }
+}

+ 69 - 0
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)
+    }
+}

+ 64 - 0
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)
+    }
+}

+ 75 - 0
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"
+        }
+    }
+}

+ 45 - 0
docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2018 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+         xmlns:p="http://www.springframework.org/schema/p"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://www.springframework.org/schema/security"
+         xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+    <!-- tag::springSecurity[] -->
+    <http>
+        <intercept-url pattern="/**" access="authenticated"/>
+        <x509 principal-extractor-ref="principalExtractor"/>
+    </http>
+    <b:bean id="principalExtractor"
+        class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
+        p:extractPrincipalNameFromEmail="true"/>
+    <!-- end::springSecurity[] -->
+
+    <user-service id="us">
+        <user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
+    </user-service>
+
+
+
+    <b:import resource="MiscHttpConfigTests-controllers.xml"/>
+</b:beans>

+ 39 - 0
docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2018 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+         xmlns:p="http://www.springframework.org/schema/p"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://www.springframework.org/schema/security"
+         xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+    <!-- tag::springSecurity[] -->
+    <http>
+        <intercept-url pattern="/**" access="authenticated"/>
+        <x509 />
+    </http>
+    <!-- end::springSecurity[] -->
+
+    <user-service>
+        <user name="rod" password="{noop}password" authorities="ROLE_USER"/>
+    </user-service>
+
+</b:beans>