diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index a3818e2a9ac..d6241a0fbe5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,7 +161,10 @@ public X509Configurer authenticationUserDetailsService( * @param subjectPrincipalRegex the regex to extract the user principal from the * certificate (i.e. "CN=(.*?)(?:,|$)"). * @return the {@link X509Configurer} for further customizations + * @deprecated Please use {{@link #x509PrincipalExtractor(X509PrincipalExtractor)} + * instead */ + @Deprecated public X509Configurer subjectPrincipalRegex(String subjectPrincipalRegex) { SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); principalExtractor.setSubjectDnRegex(subjectPrincipalRegex); diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index f997e5fa5e5..55efc57a17d 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -521,11 +521,23 @@ void createX509Filter(BeanReference authManager, filterBuilder.addPropertyValue("authenticationManager", authManager); filterBuilder.addPropertyValue("securityContextHolderStrategy", authenticationFilterSecurityContextHolderStrategyRef); - String regex = x509Elt.getAttribute("subject-principal-regex"); - if (StringUtils.hasText(regex)) { + String principalExtractorRef = x509Elt.getAttribute("principal-extractor-ref"); + String subjectPrincipalRegex = x509Elt.getAttribute("subject-principal-regex"); + boolean hasPrincipalExtractorRef = StringUtils.hasText(principalExtractorRef); + boolean hasSubjectPrincipalRegex = StringUtils.hasText(subjectPrincipalRegex); + if (hasPrincipalExtractorRef && hasSubjectPrincipalRegex) { + this.pc.getReaderContext() + .error("The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <" + + Elements.X509 + ">", this.pc.extractSource(x509Elt)); + } + if (hasPrincipalExtractorRef) { + RuntimeBeanReference principalExtractor = new RuntimeBeanReference(principalExtractorRef); + filterBuilder.addPropertyValue("principalExtractor", principalExtractor); + } + if (hasSubjectPrincipalRegex) { BeanDefinitionBuilder extractor = BeanDefinitionBuilder .rootBeanDefinition(SubjectDnX509PrincipalExtractor.class); - extractor.addPropertyValue("subjectDnRegex", regex); + extractor.addPropertyValue("subjectDnRegex", subjectPrincipalRegex); filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition()); } injectAuthenticationDetailsSource(x509Elt, filterBuilder); diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index a4063fced4b..f0760bc64e2 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -119,7 +119,7 @@ import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter; import org.springframework.security.web.PortMapper; import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.security.web.server.DefaultServerRedirectStrategy; import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; @@ -944,8 +944,8 @@ public ServerHttpSecurity formLogin(Customizer formLoginCustomize * } * * - * Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor} - * will be used. If authenticationManager is not specified, + * Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will + * be used. If authenticationManager is not specified, * {@link ReactivePreAuthenticatedAuthenticationManager} will be used. * @return the {@link X509Spec} to customize * @since 5.2 @@ -979,8 +979,8 @@ public X509Spec x509() { * } * * - * Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor} - * will be used. If authenticationManager is not specified, + * Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will + * be used. If authenticationManager is not specified, * {@link ReactivePreAuthenticatedAuthenticationManager} will be used. * @param x509Customizer the {@link Customizer} to provide more options for the * {@link X509Spec} @@ -4181,7 +4181,7 @@ private X509PrincipalExtractor getPrincipalExtractor() { if (this.principalExtractor != null) { return this.principalExtractor; } - return new SubjectDnX509PrincipalExtractor(); + return new SubjectX500PrincipalExtractor(); } private ReactiveAuthenticationManager getAuthenticationManager() { diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt index f36897604a4..514ace50b38 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt @@ -51,6 +51,7 @@ class X509Dsl { var authenticationDetailsSource: AuthenticationDetailsSource? = null var userDetailsService: UserDetailsService? = null var authenticationUserDetailsService: AuthenticationUserDetailsService? = null + @Deprecated("Use x509PrincipalExtractor instead") var subjectPrincipalRegex: String? = null diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc index bbf8622dfe2..d9a8d4f9d5f 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc @@ -1053,6 +1053,9 @@ x509.attlist &= x509.attlist &= ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter attribute authentication-details-source-ref {xsd:token}? +x509.attlist &= + ## Reference to an X509PrincipalExtractor which will be used by the authentication filter + attribute principal-extractor-ref {xsd:token}? jee = ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd index 2e3d6cf2758..4ff414800b7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd @@ -2917,6 +2917,12 @@ + + + Reference to an X509PrincipalExtractor which will be used by the authentication filter + + + diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index 206c0b7ecee..63a3a963df6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -43,7 +43,9 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -155,6 +157,28 @@ public void x509WhenStatelessSessionManagementThenDoesNotCreateSession() throws // @formatter:on } + @Test + public void x509WhenSubjectX500PrincipalExtractor() throws Exception { + this.spring.register(SubjectX500PrincipalExtractorConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull()) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + + @Test + public void x509WhenSubjectX500PrincipalExtractorBean() throws Exception { + this.spring.register(SubjectX500PrincipalExtractorEmailConfig.class).autowire(); + X509Certificate certificate = X509TestUtils.buildTestCertificate(); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull()) + .andExpect(authenticated().withUsername("luke@monkeymachine")); + // @formatter:on + } + private T loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -360,4 +384,60 @@ UserDetailsService userDetailsService() { } + @Configuration + @EnableWebSecurity + static class SubjectX500PrincipalExtractorConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509((x509) -> x509 + .x509PrincipalExtractor(new SubjectX500PrincipalExtractor()) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + } + + @Configuration + @EnableWebSecurity + static class SubjectX500PrincipalExtractorEmailConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor(); + principalExtractor.setExtractPrincipalNameFromEmail(true); + // @formatter:off + http + .x509((x509) -> x509 + .x509PrincipalExtractor(principalExtractor) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("luke@monkeymachine") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index 5221350cde0..10927906ea1 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -21,6 +21,7 @@ import java.io.OutputStream; import java.security.AccessController; import java.security.Principal; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -91,6 +92,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; @@ -398,6 +400,27 @@ public void configureWhenUsingX509ThenAddsX509FilterCorrectly() { .containsSubsequence(CsrfFilter.class, X509AuthenticationFilter.class, ExceptionTranslationFilter.class); } + @Test + public void getWhenUsingX509PrincipalExtractorRef() throws Exception { + this.spring.configLocations(xml("X509PrincipalExtractorRef")).autowire(); + X509Certificate certificate = X509TestUtils.buildTestCertificate(); + RequestPostProcessor x509 = x509(certificate); + // @formatter:off + this.mvc.perform(get("/protected").with(x509)) + .andExpect(status().isOk()); + // @formatter:on + } + + @Test + public void getWhenUsingX509PrincipalExtractorRefAndSubjectPrincipalRegex() throws Exception { + String xmlResourceName = "X509PrincipalExtractorRefAndSubjectPrincipalRegex"; + // @formatter:off + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> this.spring.configLocations(xml(xmlResourceName)).autowire()) + .withMessage("Configuration problem: The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within \n" + "Offending resource: class path resource [org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml]"); + // @formatter:on + } + @Test public void getWhenUsingX509AndPropertyPlaceholderThenSubjectPrincipalRegexIsConfigured() throws Exception { System.setProperty("subject_principal_regex", "OU=(.*?)(?:,|$)"); diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRef.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRef.xml new file mode 100644 index 00000000000..51a3a1bbf14 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRef.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml new file mode 100644 index 00000000000..1fc7e0f12c7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/antora.yml b/docs/antora.yml index 02262db72ea..c6a145aaabf 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -18,3 +18,4 @@ asciidoc: gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}" include-java: 'example$docs-src/test/java/org/springframework/security/docs' include-kotlin: 'example$docs-src/test/kotlin/org/springframework/security/kt/docs' + include-xml: 'example$docs-src/test/resources/org/springframework/security/docs' diff --git a/docs/modules/ROOT/pages/reactive/authentication/x509.adoc b/docs/modules/ROOT/pages/reactive/authentication/x509.adoc index a077dd5c65b..947084423c9 100644 --- a/docs/modules/ROOT/pages/reactive/authentication/x509.adoc +++ b/docs/modules/ROOT/pages/reactive/authentication/x509.adoc @@ -5,98 +5,16 @@ Similar to xref:servlet/authentication/x509.adoc#servlet-x509[Servlet X.509 auth The following example shows a reactive x509 security configuration: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .x509(withDefaults()) - .authorizeExchange(exchanges -> exchanges - .anyExchange().permitAll() - ); - return http.build(); -} ----- +include-code::./DefaultX509Configuration[tag=springSecurity,indent=0] -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Bean -fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - x509 { } - authorizeExchange { - authorize(anyExchange, authenticated) - } - } -} ----- -====== - -In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used. The default principal extractor is `SubjectDnX509PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client. The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired. +In the preceding configuration, when neither `principalExtractor` nor `authenticationManager` is provided, defaults are used. +The default principal extractor is `SubjectX500PrincipalExtractor`, which extracts the CN (common name) field from a certificate provided by a client. +The default authentication manager is `ReactivePreAuthenticatedAuthenticationManager`, which performs user account validation, checking that a user account with a name extracted by `principalExtractor` exists and that it is not locked, disabled, or expired. The following example demonstrates how these defaults can be overridden: -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Bean -public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - SubjectDnX509PrincipalExtractor principalExtractor = - new SubjectDnX509PrincipalExtractor(); - - 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. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 8979d5ad290..555a1b4323f 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -2218,6 +2218,10 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates * **authentication-details-source-ref** A reference to an `AuthenticationDetailsSource` +[[nsa-x509-principal-extractor-ref]] +* **principal-extractor-ref** +Reference to an `X509PrincipalExtractor` which will be used by the authentication filter. + [[nsa-x509-subject-principal-regex]] * **subject-principal-regex** diff --git a/docs/modules/ROOT/pages/servlet/authentication/x509.adoc b/docs/modules/ROOT/pages/servlet/authentication/x509.adoc index 26dbb1a9008..2d670a632e5 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/x509.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/x509.adoc @@ -14,37 +14,27 @@ You should get this working before trying it out with Spring Security. The Spring Security X.509 module extracts the certificate by using a filter. It maps the certificate to an application user and loads that user's set of granted authorities for use with the standard Spring Security infrastructure. - +[[servlet-x509-config]] == Adding X.509 Authentication to Your Web Application -Enabling X.509 client authentication is very straightforward. -To do so, add the `` element to your http security namespace configuration: -[source,xml] ----- - -... - ; - ----- +Similar to xref:reactive/authentication/x509.adoc[Reactive X.509 authentication], the servlet x509 authentication filter allows extracting an authentication token from a certificate provided by a client. + +The 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 diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java new file mode 100644 index 00000000000..8f7f8c8278c --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/CustomX509Configuration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.reactive.authentication.reactivex509; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@Configuration(proxyBeanMethods = false) +@EnableWebFluxSecurity +@EnableWebFlux +public class CustomX509Configuration { + + // tag::springSecurity[] + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor(); + principalExtractor.setExtractPrincipalNameFromEmail(true); + + // @formatter:off + UserDetails user = User + .withUsername("luke@monkeymachine") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + ReactiveUserDetailsService users = new MapReactiveUserDetailsService(user); + ReactiveAuthenticationManager authenticationManager = new ReactivePreAuthenticatedAuthenticationManager(users); + + // @formatter:off + http + .x509(x509 -> x509 + .principalExtractor(principalExtractor) + .authenticationManager(authenticationManager) + ) + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ); + // @formatter:on + return http.build(); + } + // end::springSecurity[] + +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java new file mode 100644 index 00000000000..0fd17fa1d8a --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/DefaultX509Configuration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.reactive.authentication.reactivex509; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@Configuration(proxyBeanMethods = false) +@EnableWebFluxSecurity +@EnableWebFlux +public class DefaultX509Configuration { + + // tag::springSecurity[] + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { + // @formatter:off + http + .x509(Customizer.withDefaults()) + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ); + // @formatter:on + return http.build(); + } + // end::springSecurity[] + + @Bean + ReactiveUserDetailsService userDetailsService() { + // @formatter:off + UserDetails user = User + .withUsername("rod") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + return new MapReactiveUserDetailsService(user); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java new file mode 100644 index 00000000000..475787da4f6 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/reactive/authentication/reactivex509/X509ConfigurationTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.reactive.authentication.reactivex509; + +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClientConfigurer; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * Tests {@link CustomX509Configuration}. + * + * @author Rob Winch + */ +@ExtendWith(SpringTestContextExtension.class) +public class X509ConfigurationTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + WebTestClient client; + + @Autowired + void setSpringSecurityFilterChain(WebFilter springSecurityFilterChain) { + this.client = WebTestClient.bindToController(WebTestClientBuilder.Http200RestController.class) + .webFilter(springSecurityFilterChain) + .apply(springSecurity()) + .configureClient() + .build(); + } + + @Test + void x509WhenDefaultX509Configuration() throws Exception { + this.spring.register(DefaultX509Configuration.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.client + .mutateWith(x509(certificate)) + .get() + .uri("/") + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + + @Test + void x509WhenCustomX509Configuration() throws Exception { + this.spring.register(CustomX509Configuration.class).autowire(); + X509Certificate certificate = X509TestUtils.buildTestCertificate(); + // @formatter:off + this.client + .mutateWith(x509(certificate)) + .get() + .uri("/") + .exchange() + .expectStatus().isOk(); + // @formatter:on + } + + private static @NotNull WebTestClientConfigurer x509(X509Certificate certificate) { + return (builder, httpHandlerBuilder, connector) -> { + builder.apply(new WebTestClientConfigurer() { + @Override + public void afterConfigurerAdded(WebTestClient.Builder builder, + @Nullable WebHttpHandlerBuilder httpHandlerBuilder, + @Nullable ClientHttpConnector connector) { + SslInfo sslInfo = new SslInfo() { + @Override + public @Nullable String getSessionId() { + return "sessionId"; + } + + @Override + public X509Certificate @Nullable [] getPeerCertificates() { + return new X509Certificate[] { certificate }; + } + }; + httpHandlerBuilder.filters((filters) -> filters.add(0, new SslInfoOverrideWebFilter(sslInfo))); + } + }); + }; + } + + private static class SslInfoOverrideWebFilter implements WebFilter { + private final SslInfo sslInfo; + + private SslInfoOverrideWebFilter(SslInfo sslInfo) { + this.sslInfo = sslInfo; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo) + .build(); + ServerWebExchange sslInfoExchange = exchange.mutate().request(sslInfoRequest).build(); + return chain.filter(sslInfoExchange); + } + } + + private T loadCert(String location) { + try (InputStream is = new ClassPathResource(location).getInputStream()) { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (T) certFactory.generateCertificate(is); + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java new file mode 100644 index 00000000000..70b46b7c544 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.authentication.servletx509config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class CustomX509Configuration { + + // tag::springSecurity[] + @Bean + DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor(); + principalExtractor.setExtractPrincipalNameFromEmail(true); + + + // @formatter:off + http + .x509((x509) -> x509 + .x509PrincipalExtractor(principalExtractor) + ) + .authorizeHttpRequests((exchanges) -> exchanges + .anyRequest().authenticated() + ); + // @formatter:on + return http.build(); + } + // end::springSecurity[] + + @Bean + UserDetailsService userDetailsService() { + // @formatter:off + UserDetails user = User + .withUsername("luke@monkeymachine") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java new file mode 100644 index 00000000000..a3472353869 --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.authentication.servletx509config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class DefaultX509Configuration { + + // tag::springSecurity[] + @Bean + DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509(Customizer.withDefaults()) + .authorizeHttpRequests(exchanges -> exchanges + .anyRequest().authenticated() + ); + // @formatter:on + return http.build(); + } + // end::springSecurity[] + + @Bean + UserDetailsService userDetailsService() { + // @formatter:off + UserDetails user = User + .withUsername("rod") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java new file mode 100644 index 00000000000..c6aa6f6d41b --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletx509config/X509ConfigurationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.docs.servlet.authentication.servletx509config; + +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClientConfigurer; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests {@link CustomX509Configuration}. + * + * @author Rob Winch + */ +@ExtendWith(SpringTestContextExtension.class) +public class X509ConfigurationTests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + MockMvc mockMvc; + + @Test + void x509WhenDefaultX509Configuration() throws Exception { + this.spring.register(DefaultX509Configuration.class, Http200Controller.class).autowire(); + // @formatter:off + this.mockMvc.perform(get("/").with(x509("rod.cer"))) + .andExpect(status().isOk()) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + + @Test + void x509WhenDefaultX509ConfigurationXml() throws Exception { + this.spring.testConfigLocations("DefaultX509Configuration.xml").autowire(); + // @formatter:off + this.mockMvc.perform(get("/").with(x509("rod.cer"))) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + + @Test + void x509WhenCustomX509Configuration() throws Exception { + this.spring.register(CustomX509Configuration.class, Http200Controller.class).autowire(); + X509Certificate certificate = X509TestUtils.buildTestCertificate(); + // @formatter:off + this.mockMvc.perform(get("/").with(x509(certificate))) + .andExpect(status().isOk()) + .andExpect(authenticated().withUsername("luke@monkeymachine")); + // @formatter:on + } + + @RestController + static class Http200Controller { + @GetMapping("/**") + String ok() { + return "ok"; + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt new file mode 100644 index 00000000000..78c4fbe34bf --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/CustomX509Configuration.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.reactive.authentication.reactivex509 + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity.http +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.ReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebFlux +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class CustomX509Configuration { + + // tag::springSecurity[] + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val extractor = SubjectX500PrincipalExtractor() + extractor.setExtractPrincipalNameFromEmail(true) + + // @formatter:off + val user = User + .withUsername("luke@monkeymachine") + .password("password") + .roles("USER") + .build() + // @formatter:on + + val users: ReactiveUserDetailsService = MapReactiveUserDetailsService(user) + val authentication: ReactiveAuthenticationManager = ReactivePreAuthenticatedAuthenticationManager(users) + + return http { + x509 { + principalExtractor = extractor + authenticationManager = authentication + } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + // end::springSecurity[] + + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt new file mode 100644 index 00000000000..61e5d65db5c --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/DefaultX509Configuration.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.kt.docs.reactive.authentication.reactivex509 + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebFlux +@EnableWebFluxSecurity +@Configuration(proxyBeanMethods = false) +class DefaultX509Configuration { + + // tag::springSecurity[] + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + x509 { } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + // end::springSecurity[] + + @Bean + fun userDetailsService(): MapReactiveUserDetailsService { + // @formatter:off + val user = User + .withUsername("rod") + .password("password") + .roles("USER") + .build() + // @formatter:on + + return MapReactiveUserDetailsService(user) + } + +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt new file mode 100644 index 00000000000..4802660f06b --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/reactive/authentication/reactivex509/X509ConfigurationTests.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.kt.docs.reactive.authentication.reactivex509 + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.io.ClassPathResource +import org.springframework.http.client.reactive.ClientHttpConnector +import org.springframework.http.server.reactive.SslInfo +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.WebTestClientConfigurer +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.WebFilter +import org.springframework.web.server.WebFilterChain +import org.springframework.web.server.adapter.WebHttpHandlerBuilder +import reactor.core.publisher.Mono +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.function.Consumer + +/** + * Tests [CustomX509Configuration]. + * + * @author Rob Winch + */ +@ExtendWith(SpringTestContextExtension::class) +class X509ConfigurationTests { + @JvmField + val spring: SpringTestContext = SpringTestContext(this) + + var client: WebTestClient? = null + + @Autowired + fun setSpringSecurityFilterChain(springSecurityFilterChain: WebFilter) { + this.client = WebTestClient + .bindToController(Http200RestController::class.java) + .webFilter(springSecurityFilterChain) + .apply(SecurityMockServerConfigurers.springSecurity()) + .configureClient() + .build() + } + + @Test + fun x509WhenDefaultX509Configuration() { + this.spring.register(DefaultX509Configuration::class.java).autowire() + val certificate = loadCert("rod.cer") + // @formatter:off + this.client!!.mutateWith(x509(certificate)) + .get() + .uri("/") + .exchange() + .expectStatus().isOk() + // @formatter:on + } + + @Test + fun x509WhenCustomX509Configuration() { + this.spring.register(CustomX509Configuration::class.java).autowire() + val certificate = X509TestUtils.buildTestCertificate() + // @formatter:off + this.client!!.mutateWith(x509(certificate)) + .get() + .uri("/") + .exchange() + .expectStatus().isOk() + // @formatter:on + } + + private class SslInfoOverrideWebFilter(private val sslInfo: SslInfo) : WebFilter { + override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { + val sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo) + .build() + val sslInfoExchange = exchange.mutate().request(sslInfoRequest).build() + return chain.filter(sslInfoExchange) + } + } + + private fun loadCert(location: String): T { + try { + ClassPathResource(location).getInputStream().use { `is` -> + val certFactory = CertificateFactory.getInstance("X.509") + return certFactory.generateCertificate(`is`) as T + } + } catch (ex: Exception) { + throw IllegalArgumentException(ex) + } + } + + companion object { + private fun x509(certificate: X509Certificate): WebTestClientConfigurer { + return WebTestClientConfigurer { builder: WebTestClient.Builder, httpHandlerBuilder: WebHttpHandlerBuilder, connector: ClientHttpConnector? -> + + val sslInfo: SslInfo = object : SslInfo { + override fun getSessionId(): String { + return "sessionId" + } + + override fun getPeerCertificates(): Array { + return arrayOf(certificate) + } + } + httpHandlerBuilder.filters(Consumer { filters: MutableList -> + filters.add( + 0, + SslInfoOverrideWebFilter(sslInfo) + ) + }) + } + } + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt new file mode 100644 index 00000000000..b079ce26fb5 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/CustomX509Configuration.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.kt.docs.servlet.authentication.servlet509config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class CustomX509Configuration { + // tag::springSecurity[] + @Bean + fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? { + val principalExtractor = SubjectX500PrincipalExtractor() + principalExtractor.setExtractPrincipalNameFromEmail(true) + + // @formatter:off + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + x509 { + x509PrincipalExtractor = principalExtractor + } + } + return http.build() + } + // end::springSecurity[] + + @Bean + fun userDetailsService(): UserDetailsService { + // @formatter:off + val user = User + .withUsername("luke@monkeymachine") + .password("password") + .roles("USER") + .build() + // @formatter:on + return InMemoryUserDetailsManager(user) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt new file mode 100644 index 00000000000..3c777ebe32c --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/DefaultX509Configuration.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.kt.docs.servlet.authentication.servlet509config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +/** + * Demonstrates custom configuration for x509 reactive configuration. + * + * @author Rob Winch + */ +@EnableWebMvc +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +class DefaultX509Configuration { + // tag::springSecurity[] + @Bean + fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? { + // @formatter:off + http { + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + x509 { } + } + // @formatter:on + return http.build() + } + // end::springSecurity[] + + @Bean + fun userDetailsService(): UserDetailsService { + // @formatter:off + val user = User + .withUsername("rod") + .password("password") + .roles("USER") + .build() + // @formatter:on + return InMemoryUserDetailsManager(user) + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt new file mode 100644 index 00000000000..edaf5030392 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletx509config/X509ConfigurationTests.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.kt.docs.servlet.authentication.servlet509config + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.config.test.SpringTestContext +import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors +import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +/** + * Tests [CustomX509Configuration]. + * + * @author Rob Winch + */ +@ExtendWith(SpringTestContextExtension::class) +class X509ConfigurationTests { + @JvmField + val spring: SpringTestContext = SpringTestContext(this) + + @Autowired + var mockMvc: MockMvc? = null + + @Test + @Throws(Exception::class) + fun x509WhenDefaultX509Configuration() { + this.spring.register(DefaultX509Configuration::class.java, Http200Controller::class.java).autowire() + // @formatter:off + this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer"))) + .andExpect(status().isOk()) + .andExpect(authenticated().withUsername("rod")) + // @formatter:on + } + + @Test + @Throws(Exception::class) + fun x509WhenCustomX509Configuration() { + this.spring.register(CustomX509Configuration::class.java, Http200Controller::class.java).autowire() + val certificate = X509TestUtils.buildTestCertificate() + // @formatter:off + this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509(certificate))) + .andExpect(status().isOk()) + .andExpect(authenticated().withUsername("luke@monkeymachine")) + // @formatter:on + } + + @RestController + internal class Http200Controller { + @GetMapping("/**") + fun ok(): String { + return "ok" + } + } +} diff --git a/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml new file mode 100644 index 00000000000..cc2772e34bb --- /dev/null +++ b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/CustomX509Configuration.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml new file mode 100644 index 00000000000..62621d1cabe --- /dev/null +++ b/docs/src/test/resources/org/springframework/security/docs/servlet/authentication/servletx509config/DefaultX509Configuration.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractor.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractor.java index b690af59c03..8e77082e3ca 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractor.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -43,7 +43,9 @@ * "EMAILADDRESS=jimi@hendrix.org, CN=..." giving a user name "jimi@hendrix.org" * * @author Luke Taylor + * @deprecated Please use {@link SubjectX500PrincipalExtractor} instead */ +@Deprecated public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware { protected final Log logger = LogFactory.getLog(getClass()); diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java new file mode 100644 index 00000000000..9472be2cc76 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication.preauth.x509; + +import java.security.cert.X509Certificate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.security.auth.x500.X500Principal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.core.log.LogMessage; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.util.Assert; + +/** + * Extracts the principal from the {@link X500Principal#getName(String)} returned by + * {@link X509Certificate#getSubjectX500Principal()} passed into + * {@link #extractPrincipal(X509Certificate)} depending on the value of + * {@link #setExtractPrincipalNameFromEmail(boolean)}. + * + * @author Max Batischev + * @author Rob Winch + * @since 7.0 + */ +public final class SubjectX500PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final Pattern EMAIL_SUBJECT_DN_PATTERN = Pattern.compile("OID.1.2.840.113549.1.9.1=(.*?)(?:,|$)", + Pattern.CASE_INSENSITIVE); + + private static final Pattern CN_SUBJECT_DN_PATTERN = Pattern.compile("CN=(.*?)(?:,|$)", Pattern.CASE_INSENSITIVE); + + private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + + private Pattern subjectDnPattern = CN_SUBJECT_DN_PATTERN; + + private String x500PrincipalFormat = X500Principal.RFC2253; + + @Override + public Object extractPrincipal(X509Certificate clientCert) { + Assert.notNull(clientCert, "clientCert cannot be null"); + X500Principal principal = clientCert.getSubjectX500Principal(); + String subjectDN = principal.getName(this.x500PrincipalFormat); + this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN)); + Matcher matcher = this.subjectDnPattern.matcher(subjectDN); + if (!matcher.find()) { + throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching", + new Object[] { subjectDN }, "No matching pattern was found in subject DN: {0}")); + } + String principalName = matcher.group(1); + this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName)); + return principalName; + } + + @Override + public void setMessageSource(MessageSource messageSource) { + Assert.notNull(messageSource, "messageSource cannot be null"); + this.messages = new MessageSourceAccessor(messageSource); + } + + /** + * Sets if the principal name should be extracted from the emailAddress or CN + * attribute (default). + * + * By default, the format {@link X500Principal#RFC2253} is passed to + * {@link X500Principal#getName(String)} and the principal is extracted from the CN + * attribute as defined in + * Converting + * AttributeTypeAndValue of RFC2253. + * + * If {@link #setExtractPrincipalNameFromEmail(boolean)} is {@code true}, then the + * format {@link X500Principal#RFC2253} is passed to + * {@link X500Principal#getName(String)} and the principal is extracted from the + * OID.1.2.840.113549.1.9.1 + * (emailAddress) attribute as defined in + * Section 2.3 of + * RFC1779. + * @param extractPrincipalNameFromEmail whether to extract the principal from the + * emailAddress (default false) + * @see RFC2253 + * @see RFC1779 + */ + public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) { + if (extractPrincipalNameFromEmail) { + this.subjectDnPattern = EMAIL_SUBJECT_DN_PATTERN; + this.x500PrincipalFormat = X500Principal.RFC1779; + } + else { + this.subjectDnPattern = CN_SUBJECT_DN_PATTERN; + this.x500PrincipalFormat = X500Principal.RFC2253; + } + } + +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/X509AuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/X509AuthenticationFilter.java index c49cc6ab2f6..02618d126db 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/X509AuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/X509AuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -28,7 +28,7 @@ */ public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter { - private X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + private X509PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor(); @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java new file mode 100644 index 00000000000..ebc1064fdc3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.authentication.preauth.x509; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link SubjectX500PrincipalExtractor}. + * + * @author Max Batischev + */ +public class SubjectX500PrincipalExtractorTests { + + private final SubjectX500PrincipalExtractor extractor = new SubjectX500PrincipalExtractor(); + + @Test + void extractWhenCnPatternSetThenExtractsPrincipalName() throws Exception { + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificate()); + + assertThat(principal).isEqualTo("Luke Taylor"); + } + + @Test + void extractWhenEmailPatternSetThenExtractsPrincipalName() throws Exception { + this.extractor.setExtractPrincipalNameFromEmail(true); + + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificate()); + + assertThat(principal).isEqualTo("luke@monkeymachine"); + } + + @Test + void extractWhenCnAtEndThenExtractsPrincipalName() throws Exception { + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithCnAtEnd()); + + assertThat(principal).isEqualTo("Duke"); + } + + @Test + void setMessageSourceWhenNullThenThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null)); + } + + @Test + void extractWhenCertificateIsNullThenFails() { + assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.extractPrincipal(null)); + } + +}