Update x509 Reference

- Use include-code
- Demo how to customize SubjectX500PrincipalExtractor
This commit is contained in:
Rob Winch 2025-06-11 08:41:38 -05:00
parent 7bf2730a53
commit e3add59550
17 changed files with 1118 additions and 114 deletions

View File

@ -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'

View File

@ -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.

View File

@ -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 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

View File

@ -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[]
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}
}

View File

@ -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[]
}

View File

@ -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)
}
}

View File

@ -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)
)
})
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}
}

View File

@ -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>

View File

@ -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>