Skip to content

Add support setting X509PrincipalExtractor as bean #17171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -36,6 +36,8 @@
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* Adds X509 based pre authentication to an application. Since validating the certificate
Expand Down Expand Up @@ -74,6 +76,7 @@
*
* @author Rob Winch
* @author Ngoc Nhan
* @author Max Batischev
* @since 3.2
*/
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
Expand All @@ -87,6 +90,8 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>

private AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource;

private String subjectPrincipalRegex;

/**
* Creates a new instance
*
Expand All @@ -103,6 +108,7 @@ public X509Configurer() {
* @return the {@link X509Configurer} for further customizations
*/
public X509Configurer<H> x509AuthenticationFilter(X509AuthenticationFilter x509AuthenticationFilter) {
Assert.notNull(x509AuthenticationFilter, "x509AuthenticationFilter cannot be null");
this.x509AuthenticationFilter = x509AuthenticationFilter;
return this;
}
Expand All @@ -113,6 +119,7 @@ public X509Configurer<H> x509AuthenticationFilter(X509AuthenticationFilter x509A
* @return the {@link X509Configurer} to use
*/
public X509Configurer<H> x509PrincipalExtractor(X509PrincipalExtractor x509PrincipalExtractor) {
Assert.notNull(x509PrincipalExtractor, "x509PrincipalExtractor cannot be null");
this.x509PrincipalExtractor = x509PrincipalExtractor;
return this;
}
Expand All @@ -124,6 +131,7 @@ public X509Configurer<H> x509PrincipalExtractor(X509PrincipalExtractor x509Princ
*/
public X509Configurer<H> authenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
return this;
}
Expand All @@ -150,6 +158,7 @@ public X509Configurer<H> userDetailsService(UserDetailsService userDetailsServic
*/
public X509Configurer<H> authenticationUserDetailsService(
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService) {
Assert.notNull(authenticationUserDetailsService, "authenticationUserDetailsService cannot be null");
this.authenticationUserDetailsService = authenticationUserDetailsService;
return this;
}
Expand All @@ -163,9 +172,8 @@ public X509Configurer<H> authenticationUserDetailsService(
* @return the {@link X509Configurer} for further customizations
*/
public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
this.x509PrincipalExtractor = principalExtractor;
Assert.hasText(subjectPrincipalRegex, "subjectPrincipalRegex cannot be null or empty");
this.subjectPrincipalRegex = subjectPrincipalRegex;
return this;
}

Expand All @@ -187,9 +195,7 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM
if (this.x509AuthenticationFilter == null) {
this.x509AuthenticationFilter = new X509AuthenticationFilter();
this.x509AuthenticationFilter.setAuthenticationManager(authenticationManager);
if (this.x509PrincipalExtractor != null) {
this.x509AuthenticationFilter.setPrincipalExtractor(this.x509PrincipalExtractor);
}
this.x509AuthenticationFilter.setPrincipalExtractor(getX509PrincipalExtractor(http));
if (this.authenticationDetailsSource != null) {
this.x509AuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
}
Expand All @@ -209,6 +215,22 @@ private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> ge
return this.authenticationUserDetailsService;
}

private X509PrincipalExtractor getX509PrincipalExtractor(H http) {
if (this.x509PrincipalExtractor != null) {
return this.x509PrincipalExtractor;
}
X509PrincipalExtractor extractor = getSharedOrBean(http, X509PrincipalExtractor.class);
if (extractor != null) {
return extractor;
}
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
if (StringUtils.hasText(this.subjectPrincipalRegex)) {
principalExtractor.setSubjectDnRegex(this.subjectPrincipalRegex);
}
this.x509PrincipalExtractor = principalExtractor;
return this.x509PrincipalExtractor;
}

private <C> C getSharedOrBean(H http, Class<C> type) {
C shared = http.getSharedObject(type);
if (shared != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -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.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -155,6 +157,19 @@ public void x509WhenStatelessSessionManagementThenDoesNotCreateSession() throws
// @formatter:on
}

@Test
public void x509WhenConfiguredX509PrincipalExtractorAsBeanThenUsesCustomExtractor() throws Exception {
this.spring.register(X509PrincipalExtractorBeanConfig.class).autowire();
X509Certificate certificate = loadCert("rod.cer");
// @formatter:off
this.mvc.perform(get("/").with(x509(certificate)))
.andExpect(authenticated().withUsername("rod"));
X509PrincipalExtractor extractor = this.spring.getContext().getBean(
X509PrincipalExtractorBeanConfig.CustomX509PrincipalExtractor.class);
verify(extractor).extractPrincipal(any(X509Certificate.class));
// @formatter:on
}

private <T extends Certificate> T loadCert(String location) {
try (InputStream is = new ClassPathResource(location).getInputStream()) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Expand Down Expand Up @@ -360,4 +375,47 @@ UserDetailsService userDetailsService() {

}

@Configuration
@EnableWebSecurity
static class X509PrincipalExtractorBeanConfig {

private final CustomX509PrincipalExtractor extractor = spy(CustomX509PrincipalExtractor.class);

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.x509(withDefaults());
// @formatter:on
return http.build();
}

@Bean
X509PrincipalExtractor x509PrincipalExtractor() {
return this.extractor;
}

@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("rod")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}

public static final class CustomX509PrincipalExtractor implements X509PrincipalExtractor {

private final X509PrincipalExtractor extractor = new SubjectDnX509PrincipalExtractor();

@Override
public Object extractPrincipal(X509Certificate cert) {
return this.extractor.extractPrincipal(cert);
}

}

}

}