diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index a3818e2a9ac..0358dda6e13 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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 @@ -74,6 +76,7 @@ * * @author Rob Winch * @author Ngoc Nhan + * @author Max Batischev * @since 3.2 */ public final class X509Configurer> @@ -87,6 +90,8 @@ public final class X509Configurer> private AuthenticationDetailsSource authenticationDetailsSource; + private String subjectPrincipalRegex; + /** * Creates a new instance * @@ -103,6 +108,7 @@ public X509Configurer() { * @return the {@link X509Configurer} for further customizations */ public X509Configurer x509AuthenticationFilter(X509AuthenticationFilter x509AuthenticationFilter) { + Assert.notNull(x509AuthenticationFilter, "x509AuthenticationFilter cannot be null"); this.x509AuthenticationFilter = x509AuthenticationFilter; return this; } @@ -113,6 +119,7 @@ public X509Configurer x509AuthenticationFilter(X509AuthenticationFilter x509A * @return the {@link X509Configurer} to use */ public X509Configurer x509PrincipalExtractor(X509PrincipalExtractor x509PrincipalExtractor) { + Assert.notNull(x509PrincipalExtractor, "x509PrincipalExtractor cannot be null"); this.x509PrincipalExtractor = x509PrincipalExtractor; return this; } @@ -124,6 +131,7 @@ public X509Configurer x509PrincipalExtractor(X509PrincipalExtractor x509Princ */ public X509Configurer authenticationDetailsSource( AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); this.authenticationDetailsSource = authenticationDetailsSource; return this; } @@ -150,6 +158,7 @@ public X509Configurer userDetailsService(UserDetailsService userDetailsServic */ public X509Configurer authenticationUserDetailsService( AuthenticationUserDetailsService authenticationUserDetailsService) { + Assert.notNull(authenticationUserDetailsService, "authenticationUserDetailsService cannot be null"); this.authenticationUserDetailsService = authenticationUserDetailsService; return this; } @@ -163,9 +172,8 @@ public X509Configurer authenticationUserDetailsService( * @return the {@link X509Configurer} for further customizations */ public X509Configurer 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; } @@ -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); } @@ -209,6 +215,22 @@ private AuthenticationUserDetailsService 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 getSharedOrBean(H http, Class type) { C shared = http.getSharedObject(type); if (shared != null) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index 206c0b7ecee..2a45f4c6750 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -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. @@ -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; @@ -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 loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -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); + } + + } + + } + }