From 0c6b73d123a56f1e2cb1ed10d7259ee835147254 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:10:43 -0600 Subject: [PATCH] WebAuthn Publishes Authentication Events Closes gh-18113 Signed-off-by: suuuuuuminnnnnn --- .../web/configurers/WebAuthnConfigurer.java | 7 ++- .../configurers/WebAuthnConfigurerTests.java | 63 +++++++++++++++++++ 2 files changed, 68 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..cb534a38f3 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 @@ -24,6 +24,7 @@ import java.util.Set; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.core.authority.FactorGrantedAuthority; @@ -176,8 +177,10 @@ public class WebAuthnConfigurer> WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); - webAuthnAuthnFilter.setAuthenticationManager( - new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); + ProviderManager providerManager = new ProviderManager( + new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)); + getBeanOrNull(AuthenticationEventPublisher.class).ifPresent(providerManager::setAuthenticationEventPublisher); + webAuthnAuthnFilter.setAuthenticationManager(providerManager); 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..89b978f9ca 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 @@ -30,6 +30,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpOutputMessage; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.security.authentication.AuthenticationEventPublisher; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -55,6 +58,7 @@ import org.springframework.security.web.webauthn.management.PublicKeyCredentialU 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; @@ -283,6 +287,55 @@ public class WebAuthnConfigurerTests { .andExpect(status().isForbidden()); } + @Test + public void webauthnWhenAuthenticationEventPublisherBeanThenUsed() { + this.spring.register(DefaultWebauthnConfiguration.class, CustomEventPublisherConfig.class).autowire(); + + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + WebAuthnAuthenticationFilter webAuthnFilter = filterChain.getFilterChains() + .get(0) + .getFilters() + .stream() + .filter(WebAuthnAuthenticationFilter.class::isInstance) + .map(WebAuthnAuthenticationFilter.class::cast) + .findFirst() + .orElseThrow(); + + AuthenticationManager authManager = (AuthenticationManager) ReflectionTestUtils.getField(webAuthnFilter, + "authenticationManager"); + assertThat(authManager).isInstanceOf(ProviderManager.class); + + Object publisher = ReflectionTestUtils.getField(authManager, "eventPublisher"); + AuthenticationEventPublisher expectedPublisher = this.spring.getContext() + .getBean(AuthenticationEventPublisher.class); + assertThat(publisher).isSameAs(expectedPublisher); + } + + @Test + public void webauthnWhenNoAuthenticationEventPublisherBeanThenDefaultNullPublisher() { + this.spring.register(DefaultWebauthnConfiguration.class).autowire(); + + FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class); + WebAuthnAuthenticationFilter webAuthnFilter = filterChain.getFilterChains() + .get(0) + .getFilters() + .stream() + .filter(WebAuthnAuthenticationFilter.class::isInstance) + .map(WebAuthnAuthenticationFilter.class::cast) + .findFirst() + .orElseThrow(); + + AuthenticationManager authManager = (AuthenticationManager) ReflectionTestUtils.getField(webAuthnFilter, + "authenticationManager"); + assertThat(authManager).isInstanceOf(ProviderManager.class); + + Object publisher = ReflectionTestUtils.getField(authManager, "eventPublisher"); + // ProviderManager.java:316: private static final class NullEventPublisher + assertThat(publisher).isNotNull(); + assertThat(publisher.getClass().getDeclaringClass()).isEqualTo(ProviderManager.class); + assertThat(publisher.getClass().getName()).contains("NullEventPublisher"); + } + @Configuration @EnableWebSecurity static class ConfigCredentialCreationOptionsRepository { @@ -365,6 +418,16 @@ public class WebAuthnConfigurerTests { } + @Configuration(proxyBeanMethods = false) + static class CustomEventPublisherConfig { + + @Bean + AuthenticationEventPublisher authenticationEventPublisher() { + return mock(AuthenticationEventPublisher.class); + } + + } + @Configuration(proxyBeanMethods = false) static class PostProcessorConfiguration {