Repository: spring-projects/spring-security (2026-03-19T11:09:13Z)

Issue:
WebAuthn ignores AuthenticationEventPublisher
**Describe the bug**
WebAuthn does not use the configured (global) `AuthenticationEventPublisher`.

looks like `NullEventPublisher` is always used (at least thats what you can see in debug).  
ref. https://github.com/spring-projects/spring-security/blob/7.0.0-RC1/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java#L178-L179

**To Reproduce**
* define an `AuthenticationEventPublisher` bean
* authenticate e.g. with username/password
  * `AuthenticationSuccessEvent` published on the configured `AuthenticationEventPublisher`
* authenticate with a passkey
  * `AuthenticationSuccessEvent` published on a `NullEventPublisher` (and swallowed)

**Expected behavior**
WebAuthn should use the configured (global) `AuthenticationEventPublisher`.

**Sample**

sample app will be referenced asap:
* run
* log in as user / password
  * event listener fired, see log
```
2025-10-28T07:04:30.267+01:00  INFO 32928 --- [demo] [nio-8080-exec-6] com.example.demo.AuthenticationEvents    : AuthenticationEvents onSuccess: org.springframework.security.authentication.event.AuthenticationSuccessEvent[source=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=C36A389B691D34D30439DB46090194E2], Granted Authorities=[ROLE_USER, FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=2025-10-28T06:04:30.265508800Z]]]]
```
* http://localhost:8080/webauthn/register
  * register a passkey
* log out
* log in using passkey
  * event listener not fired


Discussion:
i can take a look and try to turn the repro into a minimal fix + test. let me know if someone is already working on it.



Patch:
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 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 void configure(H http) {
 		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/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc
index 4d4a71f4e9..76314d2c87 100644
--- a/docs/modules/ROOT/pages/whats-new.adoc
+++ b/docs/modules/ROOT/pages/whats-new.adoc
@@ -16,3 +16,7 @@
 == OAuth 2.0
 
 * https://github.com/spring-projects/spring-security/issues/18745[gh-18745] - Add RestClientOpaqueTokenIntrospector
+
+== WebAuthn
+
+* https://github.com/spring-projects/spring-security/issues/18113[gh-18113] - Publish Authentication Events in WebAuthn


Test patch:
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.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.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 @@ void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws
 			.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 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 
 	}
 
+	@Configuration(proxyBeanMethods = false)
+	static class CustomEventPublisherConfig {
+
+		@Bean
+		AuthenticationEventPublisher authenticationEventPublisher() {
+			return mock(AuthenticationEventPublisher.class);
+		}
+
+	}
+
 	@Configuration(proxyBeanMethods = false)
 	static class PostProcessorConfiguration {