From f2b12e8ec8065b1606f53efa0ca9cfe699083be4 Mon Sep 17 00:00:00 2001 From: hanweiwei Date: Sat, 4 Apr 2026 13:33:42 +0800 Subject: [PATCH 1/2] Apply SessionAuthenticationStrategy to WebAuthnAuthenticationFilter WebAuthnConfigurer did not wire the shared SessionAuthenticationStrategy into WebAuthnAuthenticationFilter, so session concurrency limits were bypassed during passkey authentication. Closes gh-16685 Signed-off-by: hanweiwei --- .../web/configurers/WebAuthnConfigurer.java | 10 ++++- .../configurers/WebAuthnConfigurerTests.java | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java index d3f42cc9ca..20e107160c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -31,6 +31,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @@ -170,14 +171,19 @@ public void configure(H http) { .orElseThrow(() -> new IllegalStateException("Missing UserDetailsService Bean")); PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http, PublicKeyCredentialUserEntityRepository.class) - .orElse(userEntityRepository()); + .orElseGet(this::userEntityRepository); UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class) - .orElse(userCredentialRepository()); + .orElseGet(this::userCredentialRepository); WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); webAuthnAuthnFilter.setAuthenticationManager( new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); + SessionAuthenticationStrategy sessionAuthenticationStrategy = http + .getSharedObject(SessionAuthenticationStrategy.class); + if (sessionAuthenticationStrategy != null) { + webAuthnAuthnFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy); + } webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter); WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials, rpOperations); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java index 454d069e51..5a33957c71 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java @@ -42,6 +42,7 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.webauthn.api.Bytes; import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; @@ -408,6 +409,50 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { } + // gh-16685 + @Test + public void webAuthnWhenSessionManagementConfiguredThenSessionAuthenticationStrategyApplied() { + this.spring.register(SessionManagementWebauthnConfiguration.class).autowire(); + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + List filters = filterChain.getFilterChains().get(0).getFilters(); + WebAuthnAuthenticationFilter webAuthnFilter = filters.stream() + .filter(WebAuthnAuthenticationFilter.class::isInstance) + .map(WebAuthnAuthenticationFilter.class::cast) + .findFirst() + .orElseThrow(() -> new AssertionError("WebAuthnAuthenticationFilter not found")); + SessionAuthenticationStrategy strategy = (SessionAuthenticationStrategy) org.springframework.test.util.ReflectionTestUtils + .getField(webAuthnFilter, "sessionStrategy"); + assertThat(strategy).isNotNull(); + assertThat(strategy).isInstanceOf( + org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy.class); + } + + @Configuration + @EnableWebSecurity + static class SessionManagementWebauthnConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(Customizer.withDefaults()) + .sessionManagement((session) -> session + .maximumSessions(1) + ) + .webAuthn((authn) -> authn + .rpId("example.com") + ); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class CustomRpNameWebauthnConfiguration { From 45f54138ce9895556681524512ff86def8c68174 Mon Sep 17 00:00:00 2001 From: hanweiwei Date: Tue, 7 Apr 2026 10:16:04 +0800 Subject: [PATCH 2/2] Scope WebAuthn session strategy fix and clean test imports --- .../annotation/web/configurers/WebAuthnConfigurer.java | 4 ++-- .../web/configurers/WebAuthnConfigurerTests.java | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java index 20e107160c..72566bbb73 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -171,9 +171,9 @@ public void configure(H http) { .orElseThrow(() -> new IllegalStateException("Missing UserDetailsService Bean")); PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http, PublicKeyCredentialUserEntityRepository.class) - .orElseGet(this::userEntityRepository); + .orElse(userEntityRepository()); UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class) - .orElseGet(this::userCredentialRepository); + .orElse(userCredentialRepository()); WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java index 5a33957c71..08c8a05207 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java @@ -42,6 +42,7 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; import org.springframework.security.web.webauthn.api.Bytes; @@ -56,6 +57,7 @@ import org.springframework.security.web.webauthn.management.UserCredentialRepository; import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -420,11 +422,10 @@ public void webAuthnWhenSessionManagementConfiguredThenSessionAuthenticationStra .map(WebAuthnAuthenticationFilter.class::cast) .findFirst() .orElseThrow(() -> new AssertionError("WebAuthnAuthenticationFilter not found")); - SessionAuthenticationStrategy strategy = (SessionAuthenticationStrategy) org.springframework.test.util.ReflectionTestUtils + SessionAuthenticationStrategy strategy = (SessionAuthenticationStrategy) ReflectionTestUtils .getField(webAuthnFilter, "sessionStrategy"); assertThat(strategy).isNotNull(); - assertThat(strategy).isInstanceOf( - org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy.class); + assertThat(strategy).isInstanceOf(CompositeSessionAuthenticationStrategy.class); } @Configuration