Backport SubjectX500PrincipalExtractor
This commit backports SubjectX500PrincipalExtractor so as to provide folks moving from 6.x to 7.x a migration path from SubjectDnX509PrincipalExtractor. Issue gh-16980 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
+4
@@ -74,6 +74,7 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextR
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Ngoc Nhan
|
||||
* @author Max Batischev
|
||||
* @since 3.2
|
||||
*/
|
||||
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
|
||||
@@ -161,7 +162,10 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
|
||||
* @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<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
|
||||
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
|
||||
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
|
||||
|
||||
+15
-3
@@ -521,11 +521,23 @@ final class AuthenticationConfigBuilder {
|
||||
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);
|
||||
|
||||
+6
-6
@@ -119,7 +119,7 @@ import org.springframework.security.oauth2.server.resource.web.server.BearerToke
|
||||
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;
|
||||
@@ -943,8 +943,8 @@ public class ServerHttpSecurity {
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* 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
|
||||
@@ -978,8 +978,8 @@ public class ServerHttpSecurity {
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* 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}
|
||||
@@ -4180,7 +4180,7 @@ public class ServerHttpSecurity {
|
||||
if (this.principalExtractor != null) {
|
||||
return this.principalExtractor;
|
||||
}
|
||||
return new SubjectDnX509PrincipalExtractor();
|
||||
return new SubjectX500PrincipalExtractor();
|
||||
}
|
||||
|
||||
private ReactiveAuthenticationManager getAuthenticationManager() {
|
||||
|
||||
@@ -1050,6 +1050,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.
|
||||
|
||||
@@ -2911,6 +2911,12 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="principal-extractor-ref" type="xs:token">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Reference to an X509PrincipalExtractor which will be used by the authentication filter
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="jee">
|
||||
<xs:annotation>
|
||||
|
||||
+80
@@ -43,7 +43,9 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
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;
|
||||
@@ -123,6 +125,28 @@ public class X509ConfigurerTests {
|
||||
// @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
|
||||
}
|
||||
|
||||
@Test
|
||||
public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception {
|
||||
this.spring.register(UserDetailsServiceBeanConfig.class).autowire();
|
||||
@@ -277,6 +301,62 @@ public class X509ConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
static class UserDetailsServiceBeanConfig {
|
||||
|
||||
+23
@@ -21,6 +21,7 @@ import java.io.IOException;
|
||||
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.AnonymousAuthenticationFi
|
||||
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 class MiscHttpConfigTests {
|
||||
.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 <x509>\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=(.*?)(?:,|$)");
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?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">
|
||||
|
||||
<http>
|
||||
<x509 principal-extractor-ref="principalExtractor"/>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
</http>
|
||||
|
||||
<user-service id="us">
|
||||
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
|
||||
</user-service>
|
||||
|
||||
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
|
||||
p:extractPrincipalNameFromEmail="true"/>
|
||||
|
||||
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
|
||||
</b:beans>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?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">
|
||||
|
||||
<http>
|
||||
<x509 principal-extractor-ref="principalExtractor" subject-principal-regex="(.*)"/>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
</http>
|
||||
|
||||
<user-service id="us">
|
||||
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
|
||||
</user-service>
|
||||
|
||||
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
|
||||
p:extractPrincipalNameFromEmail="true"/>
|
||||
|
||||
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
|
||||
</b:beans>
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2212,6 +2212,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**
|
||||
@@ -2223,7 +2227,6 @@ Defines a regular expression which will be used to extract the username from the
|
||||
Allows a specific `UserDetailsService` to be used with X.509 in the case where multiple instances are configured.
|
||||
If not set, an attempt will be made to locate a suitable instance automatically and use that.
|
||||
|
||||
|
||||
[[nsa-filter-chain-map]]
|
||||
== <filter-chain-map>
|
||||
Used to explicitly configure a FilterChainProxy instance with a FilterChainMap
|
||||
|
||||
@@ -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
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.reactive.authentication.reactivex509;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFluxSecurity
|
||||
@EnableWebFlux
|
||||
public class CustomX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
|
||||
principalExtractor.setExtractPrincipalNameFromEmail(true);
|
||||
|
||||
// @formatter:off
|
||||
UserDetails user = User
|
||||
.withUsername("luke@monkeymachine")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
ReactiveUserDetailsService users = new MapReactiveUserDetailsService(user);
|
||||
ReactiveAuthenticationManager authenticationManager = new ReactivePreAuthenticatedAuthenticationManager(users);
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.x509(x509 -> x509
|
||||
.principalExtractor(principalExtractor)
|
||||
.authenticationManager(authenticationManager)
|
||||
)
|
||||
.authorizeExchange(exchanges -> exchanges
|
||||
.anyExchange().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.reactive.authentication.reactivex509;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebFluxSecurity
|
||||
@EnableWebFlux
|
||||
public class DefaultX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.x509(Customizer.withDefaults())
|
||||
.authorizeExchange(exchanges -> exchanges
|
||||
.anyExchange().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
ReactiveUserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = User
|
||||
.withUsername("rod")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new MapReactiveUserDetailsService(user);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.reactive.authentication.reactivex509;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.SslInfo;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class X509ConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@Autowired
|
||||
void setSpringSecurityFilterChain(WebFilter springSecurityFilterChain) {
|
||||
this.client = WebTestClient.bindToController(WebTestClientBuilder.Http200RestController.class)
|
||||
.webFilter(springSecurityFilterChain)
|
||||
.apply(springSecurity())
|
||||
.configureClient()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void x509WhenDefaultX509Configuration() throws Exception {
|
||||
this.spring.register(DefaultX509Configuration.class).autowire();
|
||||
X509Certificate certificate = loadCert("rod.cer");
|
||||
// @formatter:off
|
||||
this.client
|
||||
.mutateWith(x509(certificate))
|
||||
.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void x509WhenCustomX509Configuration() throws Exception {
|
||||
this.spring.register(CustomX509Configuration.class).autowire();
|
||||
X509Certificate certificate = X509TestUtils.buildTestCertificate();
|
||||
// @formatter:off
|
||||
this.client
|
||||
.mutateWith(x509(certificate))
|
||||
.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private static @NotNull WebTestClientConfigurer x509(X509Certificate certificate) {
|
||||
return (builder, httpHandlerBuilder, connector) -> {
|
||||
builder.apply(new WebTestClientConfigurer() {
|
||||
@Override
|
||||
public void afterConfigurerAdded(WebTestClient.Builder builder,
|
||||
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
|
||||
@Nullable ClientHttpConnector connector) {
|
||||
SslInfo sslInfo = new SslInfo() {
|
||||
@Override
|
||||
public @Nullable String getSessionId() {
|
||||
return "sessionId";
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate @Nullable [] getPeerCertificates() {
|
||||
return new X509Certificate[] { certificate };
|
||||
}
|
||||
};
|
||||
httpHandlerBuilder.filters((filters) -> filters.add(0, new SslInfoOverrideWebFilter(sslInfo)));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private static class SslInfoOverrideWebFilter implements WebFilter {
|
||||
private final SslInfo sslInfo;
|
||||
|
||||
private SslInfoOverrideWebFilter(SslInfo sslInfo) {
|
||||
this.sslInfo = sslInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
ServerHttpRequest sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
|
||||
.build();
|
||||
ServerWebExchange sslInfoExchange = exchange.mutate().request(sslInfoRequest).build();
|
||||
return chain.filter(sslInfoExchange);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Certificate> T loadCert(String location) {
|
||||
try (InputStream is = new ClassPathResource(location).getInputStream()) {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
return (T) certFactory.generateCertificate(is);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.servletx509config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class CustomX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
|
||||
principalExtractor.setExtractPrincipalNameFromEmail(true);
|
||||
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.x509((x509) -> x509
|
||||
.x509PrincipalExtractor(principalExtractor)
|
||||
)
|
||||
.authorizeHttpRequests((exchanges) -> exchanges
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = User
|
||||
.withUsername("luke@monkeymachine")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.servletx509config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class DefaultX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.x509(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(exchanges -> exchanges
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails user = User
|
||||
.withUsername("rod")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.docs.servlet.authentication.servletx509config;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.SslInfo;
|
||||
import org.springframework.security.config.test.SpringTestContext;
|
||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
|
||||
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomX509Configuration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class X509ConfigurationTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void x509WhenDefaultX509Configuration() throws Exception {
|
||||
this.spring.register(DefaultX509Configuration.class, Http200Controller.class).autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(x509("rod.cer")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("rod"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void x509WhenDefaultX509ConfigurationXml() throws Exception {
|
||||
this.spring.testConfigLocations("DefaultX509Configuration.xml").autowire();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(x509("rod.cer")))
|
||||
.andExpect(authenticated().withUsername("rod"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
void x509WhenCustomX509Configuration() throws Exception {
|
||||
this.spring.register(CustomX509Configuration.class, Http200Controller.class).autowire();
|
||||
X509Certificate certificate = X509TestUtils.buildTestCertificate();
|
||||
// @formatter:off
|
||||
this.mockMvc.perform(get("/").with(x509(certificate)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("luke@monkeymachine"));
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
String ok() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.ReactiveAuthenticationManager
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.web.server.invoke
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.http
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
val extractor = SubjectX500PrincipalExtractor()
|
||||
extractor.setExtractPrincipalNameFromEmail(true)
|
||||
|
||||
// @formatter:off
|
||||
val user = User
|
||||
.withUsername("luke@monkeymachine")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
// @formatter:on
|
||||
|
||||
val users: ReactiveUserDetailsService = MapReactiveUserDetailsService(user)
|
||||
val authentication: ReactiveAuthenticationManager = ReactivePreAuthenticatedAuthenticationManager(users)
|
||||
|
||||
return http {
|
||||
x509 {
|
||||
principalExtractor = extractor
|
||||
authenticationManager = authentication
|
||||
}
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.config.web.server.invoke
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebFlux
|
||||
@EnableWebFluxSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class DefaultX509Configuration {
|
||||
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http {
|
||||
x509 { }
|
||||
authorizeExchange {
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): MapReactiveUserDetailsService {
|
||||
// @formatter:off
|
||||
val user = User
|
||||
.withUsername("rod")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
// @formatter:on
|
||||
|
||||
return MapReactiveUserDetailsService(user)
|
||||
}
|
||||
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.reactive.authentication.reactivex509
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.core.io.ClassPathResource
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector
|
||||
import org.springframework.http.server.reactive.SslInfo
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder.Http200RestController
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
import org.springframework.test.web.reactive.server.WebTestClientConfigurer
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import org.springframework.web.server.WebFilter
|
||||
import org.springframework.web.server.WebFilterChain
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
|
||||
import reactor.core.publisher.Mono
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class X509ConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
var client: WebTestClient? = null
|
||||
|
||||
@Autowired
|
||||
fun setSpringSecurityFilterChain(springSecurityFilterChain: WebFilter) {
|
||||
this.client = WebTestClient
|
||||
.bindToController(Http200RestController::class.java)
|
||||
.webFilter<WebTestClient.ControllerSpec>(springSecurityFilterChain)
|
||||
.apply<WebTestClient.ControllerSpec>(SecurityMockServerConfigurers.springSecurity())
|
||||
.configureClient()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun x509WhenDefaultX509Configuration() {
|
||||
this.spring.register(DefaultX509Configuration::class.java).autowire()
|
||||
val certificate = loadCert<X509Certificate>("rod.cer")
|
||||
// @formatter:off
|
||||
this.client!!.mutateWith(x509(certificate))
|
||||
.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
fun x509WhenCustomX509Configuration() {
|
||||
this.spring.register(CustomX509Configuration::class.java).autowire()
|
||||
val certificate = X509TestUtils.buildTestCertificate()
|
||||
// @formatter:off
|
||||
this.client!!.mutateWith(x509(certificate))
|
||||
.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private class SslInfoOverrideWebFilter(private val sslInfo: SslInfo) : WebFilter {
|
||||
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||
val sslInfoRequest = exchange.getRequest().mutate().sslInfo(sslInfo)
|
||||
.build()
|
||||
val sslInfoExchange = exchange.mutate().request(sslInfoRequest).build()
|
||||
return chain.filter(sslInfoExchange)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Certificate?> loadCert(location: String): T {
|
||||
try {
|
||||
ClassPathResource(location).getInputStream().use { `is` ->
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
return certFactory.generateCertificate(`is`) as T
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
throw IllegalArgumentException(ex)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun x509(certificate: X509Certificate): WebTestClientConfigurer {
|
||||
return WebTestClientConfigurer { builder: WebTestClient.Builder, httpHandlerBuilder: WebHttpHandlerBuilder?, connector: ClientHttpConnector? ->
|
||||
|
||||
val sslInfo: SslInfo = object : SslInfo {
|
||||
override fun getSessionId(): String {
|
||||
return "sessionId"
|
||||
}
|
||||
|
||||
override fun getPeerCertificates(): Array<X509Certificate?> {
|
||||
return arrayOf(certificate)
|
||||
}
|
||||
}
|
||||
httpHandlerBuilder?.filters(Consumer { filters: MutableList<WebFilter> ->
|
||||
filters.add(
|
||||
0,
|
||||
SslInfoOverrideWebFilter(sslInfo)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomX509Configuration {
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
|
||||
val principalExtractor = SubjectX500PrincipalExtractor()
|
||||
principalExtractor.setExtractPrincipalNameFromEmail(true)
|
||||
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
x509 {
|
||||
x509PrincipalExtractor = principalExtractor
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
// @formatter:off
|
||||
val user = User
|
||||
.withUsername("luke@monkeymachine")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
// @formatter:on
|
||||
return InMemoryUserDetailsManager(user)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
/**
|
||||
* Demonstrates custom configuration for x509 reactive configuration.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class DefaultX509Configuration {
|
||||
// tag::springSecurity[]
|
||||
@Bean
|
||||
fun springSecurity(http: HttpSecurity): DefaultSecurityFilterChain? {
|
||||
// @formatter:off
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
x509 { }
|
||||
}
|
||||
// @formatter:on
|
||||
return http.build()
|
||||
}
|
||||
// end::springSecurity[]
|
||||
|
||||
@Bean
|
||||
fun userDetailsService(): UserDetailsService {
|
||||
// @formatter:off
|
||||
val user = User
|
||||
.withUsername("rod")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build()
|
||||
// @formatter:on
|
||||
return InMemoryUserDetailsManager(user)
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.kt.docs.servlet.authentication.servlet509config
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.security.config.test.SpringTestContext
|
||||
import org.springframework.security.config.test.SpringTestContextExtension
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
|
||||
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
/**
|
||||
* Tests [CustomX509Configuration].
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension::class)
|
||||
class X509ConfigurationTests {
|
||||
@JvmField
|
||||
val spring: SpringTestContext = SpringTestContext(this)
|
||||
|
||||
@Autowired
|
||||
var mockMvc: MockMvc? = null
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun x509WhenDefaultX509Configuration() {
|
||||
this.spring.register(DefaultX509Configuration::class.java, Http200Controller::class.java).autowire()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509("rod.cer")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("rod"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun x509WhenCustomX509Configuration() {
|
||||
this.spring.register(CustomX509Configuration::class.java, Http200Controller::class.java).autowire()
|
||||
val certificate = X509TestUtils.buildTestCertificate()
|
||||
// @formatter:off
|
||||
this.mockMvc!!.perform(get("/").with(SecurityMockMvcRequestPostProcessors.x509(certificate)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(authenticated().withUsername("luke@monkeymachine"))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@RestController
|
||||
internal class Http200Controller {
|
||||
@GetMapping("/**")
|
||||
fun ok(): String {
|
||||
return "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2002-2018 the original author or authors.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/security"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/security
|
||||
https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<!-- tag::springSecurity[] -->
|
||||
<http>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
<x509 principal-extractor-ref="principalExtractor"/>
|
||||
</http>
|
||||
<b:bean id="principalExtractor"
|
||||
class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
|
||||
p:extractPrincipalNameFromEmail="true"/>
|
||||
<!-- end::springSecurity[] -->
|
||||
|
||||
<user-service id="us">
|
||||
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
|
||||
</user-service>
|
||||
|
||||
|
||||
|
||||
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
|
||||
</b:beans>
|
||||
+39
@@ -0,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>
|
||||
+2
@@ -43,7 +43,9 @@ import org.springframework.util.Assert;
|
||||
* "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());
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2004-present 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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.Rdn;
|
||||
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 6.5.11
|
||||
*/
|
||||
public final class SubjectX500PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private static final String EMAIL_SUBJECT_DN_TYPE = "OID.1.2.840.113549.1.9.1";
|
||||
|
||||
private static final String CN_SUBJECT_DN_TYPE = "CN";
|
||||
|
||||
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
|
||||
|
||||
private String subjectDnType = CN_SUBJECT_DN_TYPE;
|
||||
|
||||
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));
|
||||
String principalName = getSubject(subjectDN);
|
||||
this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName));
|
||||
return principalName;
|
||||
}
|
||||
|
||||
private List<Rdn> getDns(String subjectDn) {
|
||||
try {
|
||||
// read most-specific first, see gh-19254
|
||||
List<Rdn> rdns = new ArrayList<>(new LdapName(subjectDn).getRdns());
|
||||
Collections.reverse(rdns);
|
||||
return rdns;
|
||||
}
|
||||
catch (InvalidNameException ex) {
|
||||
throw new BadCredentialsException("Failed to parse client certificate", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String getSubject(String subjectDn) {
|
||||
for (Rdn rdn : getDns(subjectDn)) {
|
||||
String type = rdn.getType();
|
||||
if (this.subjectDnType.equals(type)) {
|
||||
return String.valueOf(rdn.getValue());
|
||||
}
|
||||
}
|
||||
throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching",
|
||||
new Object[] { subjectDn }, "No matching pattern was found in subject DN: {0}"));
|
||||
}
|
||||
|
||||
@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
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc2253#section-2.3">Converting
|
||||
* AttributeTypeAndValue of RFC2253</a>.
|
||||
*
|
||||
* 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
|
||||
* <a href="https://oid-base.com/get/1.2.840.113549.1.9.1">OID.1.2.840.113549.1.9.1
|
||||
* (emailAddress)</a> attribute as defined in
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc1779#section-2.3">Section 2.3 of
|
||||
* RFC1779</a>.
|
||||
* @param extractPrincipalNameFromEmail whether to extract the principal from the
|
||||
* emailAddress (default false)
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2253">RFC2253</a>
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfC1779">RFC1779</a>
|
||||
*/
|
||||
public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) {
|
||||
if (extractPrincipalNameFromEmail) {
|
||||
this.subjectDnType = EMAIL_SUBJECT_DN_TYPE;
|
||||
this.x500PrincipalFormat = X500Principal.RFC1779;
|
||||
}
|
||||
else {
|
||||
this.subjectDnType = CN_SUBJECT_DN_TYPE;
|
||||
this.x500PrincipalFormat = X500Principal.RFC2253;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ import org.springframework.security.web.authentication.preauth.AbstractPreAuthen
|
||||
*/
|
||||
public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
|
||||
|
||||
private X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
|
||||
private X509PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
|
||||
|
||||
@Override
|
||||
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
|
||||
|
||||
+7
@@ -74,6 +74,13 @@ public class SubjectDnX509PrincipalExtractorTests {
|
||||
assertThat(principal).isEqualTo("Duke");
|
||||
}
|
||||
|
||||
// gh-19254
|
||||
@Test
|
||||
public void defaultCNPatternReturnsMostSpecificPrincipalWhenMultipleCns() throws Exception {
|
||||
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns());
|
||||
assertThat(principal).isEqualTo("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMessageSourceWhenNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2004-present 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 extractWhenDnEmbeddedInCnThenExtractsPrincipalName() throws Exception {
|
||||
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedDn());
|
||||
|
||||
assertThat(principal).isEqualTo("luke");
|
||||
}
|
||||
|
||||
// gh-19254
|
||||
@Test
|
||||
void extractWhenMultipleCnsThenExtractsMostSpecificCn() throws Exception {
|
||||
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns());
|
||||
|
||||
assertThat(principal).isEqualTo("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractWhenEmailDnEmbeddedInCnThenExtractsEmail() throws Exception {
|
||||
this.extractor.setExtractPrincipalNameFromEmail(true);
|
||||
|
||||
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedEmailDn());
|
||||
|
||||
assertThat(principal).isEqualTo("luke@monkeymachine");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMessageSourceWhenNullThenThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractWhenCertificateIsNullThenFails() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.extractPrincipal(null));
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -135,4 +135,88 @@ public final class X509TestUtils {
|
||||
return (X509Certificate) cf.generateCertificate(in);
|
||||
}
|
||||
|
||||
public static X509Certificate buildTestCertficateWithEmbeddedDn() throws Exception {
|
||||
String cert = "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIIDDTCCAfWgAwIBAgIJANSyvk4gJhqPMA0GCSqGSIb3DQEBCwUAMEYxDTALBgNV\n"
|
||||
+ "BAMMBGx1a2UxETAPBgNVBAsMCENOPWR1a2UsMRUwEwYDVQQKDAxFeGFtcGxlIENv\n"
|
||||
+ "cnAxCzAJBgNVBAYTAlVTMB4XDTI2MDEwNDE5MjY0N1oXDTI3MDEwNTE5MjY0N1ow\n"
|
||||
+ "RjENMAsGA1UEAwwEbHVrZTERMA8GA1UECwwIQ049ZHVrZSwxFTATBgNVBAoMDEV4\n"
|
||||
+ "YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n"
|
||||
+ "ggEKAoIBAQDU9fY74nEFbBKfIef7CK02J/BJb42sIF9kD8eHN5OvEwLQBeTh30it\n"
|
||||
+ "E7LLalXyOXeUFkPe1N1ZhGdVak9udsIqULSvQaWqTbN+IrAGklZAxuXYTC1GbhMF\n"
|
||||
+ "AkGWWM55J2SNqVGQaHzZUn6VPxWaDft6nZR0DxuvXMYM5kVG6VErdB3ygGUv8cjQ\n"
|
||||
+ "QBKAYpsZeRldnauRPt2dImmGTagvSuJVyr8X/AioE2Rl0guii456AKw+QSvRiZ+g\n"
|
||||
+ "w08Y8C9nDyzQmurqpdYYkp0X+4yqm1iVowMX+tSPvHnlqJdvVzaW2b0yRzrrT6ao\n"
|
||||
+ "UCgw25slR1P1IcyzqPKWQIoQRnYIaX1bAgMBAAEwDQYJKoZIhvcNAQELBQADggEB\n"
|
||||
+ "AIos+nr8DFM6bAt9AI/79O/12hcN7gVv4F3P4Vz6NRRkkvsb9WMN8fLLDEsEJ/BQ\n"
|
||||
+ "eQkAVnhlmAe++vrqy8OTHoQ7F5C3K0zrr19NLNoyNFTkXkFgnm4ZhYinSbusuIb7\n"
|
||||
+ "LPYoyCnEEiMdl0VMWWSWcOvZpipbvTtH3CiVxTqXLjFFNraEAyUN50kXjo/zuHpK\n"
|
||||
+ "HzTS1BAu0li9GdV3Da2ELdDx90zaUym7dDIejY4YUlXYIJ5UUYS61fqtgOHGLLdb\n"
|
||||
+ "UXGAr5gqEe7OrQ9D4ebg9w5ciTb7g1H2CmirjTf/rkii8AojmsGFKIfGVe3gY6EB\n" + "o9eF5FV9V9leo5yLo25ev08=\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) cf.generateCertificate(in);
|
||||
}
|
||||
|
||||
public static X509Certificate buildTestCertficateWithEmbeddedEmailDn() throws Exception {
|
||||
String cert = "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIIDfDCCAmSgAwIBAgIIXHoOUFeZ29MwDQYJKoZIhvcNAQELBQAwfjEhMB8GCSqG\n"
|
||||
+ "SIb3DQEJARYSbHVrZUBtb25rZXltYWNoaW5lMTUwMwYDVQQLDCxPSUQuMS4yLjg0\n"
|
||||
+ "MC4xMTM1NDkuMS45LjE9ZHVrZUBnb3JpbGxhZ2FkZ2V0LDEVMBMGA1UECgwMRXhh\n"
|
||||
+ "bXBsZSBDb3JwMQswCQYDVQQGEwJVUzAeFw0yNjAxMDQxOTMxMDhaFw0yNzAxMDUx\n"
|
||||
+ "OTMxMDhaMH4xITAfBgkqhkiG9w0BCQEWEmx1a2VAbW9ua2V5bWFjaGluZTE1MDMG\n"
|
||||
+ "A1UECwwsT0lELjEuMi44NDAuMTEzNTQ5LjEuOS4xPWR1a2VAZ29yaWxsYWdhZGdl\n"
|
||||
+ "dCwxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqG\n"
|
||||
+ "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBuIQWnj+uvvG+4ZIyFMs4dSbiBavubCmC\n"
|
||||
+ "hudrHr93hP19QbPulbHRTVCUqEi8efvq+J9jmMdPd7tziuDX02PeG9uljp9+c5Ir\n"
|
||||
+ "pw9/oMoTRkF7K4PK1JxLN4tcgxjxVA4QkS+MjKLPeHrYyGCjKspcHbi+zBiQ9Xqp\n"
|
||||
+ "yHWq6N5XPd6mEj2gh0zamnsJCeUCOX4SJbcp3MFtcYzhguHAeVhy9Jv+EAMJejDn\n"
|
||||
+ "YIZmMUdP6Ykf2zTzs/4L3bRZb0oS5WvfeRdJB6SKg8mNO/jdGX87krSio//cRdDy\n"
|
||||
+ "TGQK+YCVDf8GyLLavYZW56AJbZxL3MWgHYilQjj4p+Kw/PWpaBVvAgMBAAEwDQYJ\n"
|
||||
+ "KoZIhvcNAQELBQADggEBAKVTMIo8JO0H0HRrpsEDP17E2pnfMJV4g70BwClUMMek\n"
|
||||
+ "wNIWZn+6XPR8oObzzjnVWXjrovMkmmyFk0vWIpF68MPyiQ++5fwdzOZiQtUP177n\n"
|
||||
+ "9ulAtLoIJld3olGeL9VsCZGp3J2PqiDe613zd+bkSUG1lQYC2awozWqJEdvwJJtf\n"
|
||||
+ "j9nlhyMsARKEEu3tFGJsCHST3XhbhFKOraf/GZ21xW650R7ap0ZNaEiB16M2a5Oe\n"
|
||||
+ "WXasgUukIo82Z8+yK4IITeCcr0aA1fJxwhU8J6qfYWloaoirSYj487HRnPPv3X/b\n"
|
||||
+ "RxZynIjtGKygT6T1dRaWennmoitqfprJnEO2tlhLwP0=\n" + "-----END CERTIFICATE-----";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) cf.generateCertificate(in);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an X.509 certificate whose subject contains more than one CN. The subject DN
|
||||
* is:
|
||||
*
|
||||
* <pre>
|
||||
* CN=alice, CN=bob, O=Example Corp, C=US
|
||||
* </pre>
|
||||
*
|
||||
* where {@code CN=alice} is the most specific (left-most) RDN.
|
||||
*/
|
||||
public static X509Certificate buildTestCertificateWithMultipleCns() throws Exception {
|
||||
String cert = "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIIDJzCCAg+gAwIBAgIIclOz1VulWC8wDQYJKoZIhvcNAQEMBQAwQjELMAkGA1UE\n"
|
||||
+ "BhMCVVMxFTATBgNVBAoTDEV4YW1wbGUgQ29ycDEMMAoGA1UEAxMDYm9iMQ4wDAYD\n"
|
||||
+ "VQQDEwVhbGljZTAeFw0yNjA2MDExOTQzMTVaFw0zNjA1MjkxOTQzMTVaMEIxCzAJ\n"
|
||||
+ "BgNVBAYTAlVTMRUwEwYDVQQKEwxFeGFtcGxlIENvcnAxDDAKBgNVBAMTA2JvYjEO\n"
|
||||
+ "MAwGA1UEAxMFYWxpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCX\n"
|
||||
+ "q8hZrTRHJEN7+D6yK65OKeCTVU+WccI6awz6g4T6O3aoC+1IiUljsEYn+xuDfx5L\n"
|
||||
+ "L/O1kejjmbYt+vzRmiILoJ9xKfW3ERcB4+gEar959Dkj6wmpsgOKRjmOvcOFOkEe\n"
|
||||
+ "gU1F7t04JHOou3DaAkHMNMQV+3jsWSh9Rry7XZBDkcT8XbbagoCgSIIef03Qtw31\n"
|
||||
+ "zOBwqmJmd6CQhFJva5cl8cCE2xZinOIiz/2j7VprZTjhkud2M4bRnK3T0WExyOCg\n"
|
||||
+ "jvjSf5ZoOKwC2Z9Q/Oyf8WKpren1+GfZhAKKmn7QZ2foHhdPPRtNjRGE6SqQyW2u\n"
|
||||
+ "8+1tOXl+aRCF19rR7gIpAgMBAAGjITAfMB0GA1UdDgQWBBRfLtnK5WKU0q3zxIrr\n"
|
||||
+ "y3GD+lYplzANBgkqhkiG9w0BAQwFAAOCAQEAe+/FHqErVPsF/sHrVHny8mIsn3ux\n"
|
||||
+ "qE9P24KNF0oIfmBrAqqge6hoVQ8PS+JialyqFf//osuDjiuYaBEKBw7GCoA6I8mr\n"
|
||||
+ "FA7wyFaGosfq7An5vxkJl7lap2u5oSVv3dCy13Bs0ziYmNlTkfHDLy9yh7jpH1wg\n"
|
||||
+ "TvylH9O4Vc7y9rzzpIjMCuQJ/wJ4MuJ2mSarYZsx3UQHIRfpKtR/9jbFMX1Rbv/A\n"
|
||||
+ "N0XD+NrtFjiikp71y3aCO1EHnGG7qPKCWh3PzaNoWFpyZKDBvud8ymW7RMiLPgv9\n"
|
||||
+ "eTa82KGgnCwtKCNzGkszIn7fza/6xnCuzvh4y9tYE5BWP2mcMRtRmDD07w==\n" + "-----END CERTIFICATE-----";
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate) cf.generateCertificate(in);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user