From 780f35266df9ac0e116f3e6874baaa6c607c9c91 Mon Sep 17 00:00:00 2001 From: Yusuf Hussien Date: Tue, 20 Oct 2026 20:04:42 +0300 Subject: [PATCH] Fix spurious ProviderManager failure event ProviderManager synthesized a ProviderNotFoundException and published it as an AbstractAuthenticationFailureEvent whenever none of its own providers supported the token. When the manager is part of a parent/child hierarchy, this emitted a spurious event (and an extra event) that masked the actual AuthenticationException produced by the manager that handled the request. Skip publishing the failure event when the exception is the synthesized ProviderNotFoundException, so only the manager that ultimately propagates the AuthenticationException publishes it. Closes gh-19434 Signed-off-by: Yusuf Hussien --- .../authentication/ProviderManager.java | 8 ++++++- .../authentication/ProviderManagerTests.java | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index 5cbd8fb300b..c144e38844e 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -244,15 +244,21 @@ public Authentication authenticate(Authentication authentication) throws Authent } // Parent was null, or didn't authenticate (or throw an exception). + boolean providerNotFound = false; if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); + // This exception is synthesized because no provider here supports the + // token. In a parent/child hierarchy the manager that actually handled + // the request publishes the failure event, so publishing this one would + // emit a spurious event that masks the real AuthenticationException. + providerNotFound = true; } // If the parent AuthenticationManager was attempted and failed then it will // publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the // parent AuthenticationManager already published it - if (parentException == null) { + if (parentException == null && !providerNotFound) { prepareException(lastException, authentication); } diff --git a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java index 4c66bf5bae4..48b35ce6675 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -328,6 +328,28 @@ void authenticateWhenFailsInParentAndPublishesThenChildDoesNotPublish() { verifyNoMoreInteractions(publisher); // Child should not publish (duplicate event) } + // gh-19434 + @Test + void providerNotFoundFromParentPublishesOnlyChildException() { + ProviderManager parentMgr = new ProviderManager(List.of(createProviderWhichDoesNotSupport())); + BadCredentialsException childEx = new BadCredentialsException("Bad Credentials in child"); + ProviderManager childMgr = new ProviderManager(List.of(createProviderWhichThrows(childEx)), parentMgr); + AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); + parentMgr.setAuthenticationEventPublisher(publisher); + childMgr.setAuthenticationEventPublisher(publisher); + Authentication authReq = mock(Authentication.class); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> childMgr.authenticate(authReq)) + .isSameAs(childEx); + verify(publisher).publishAuthenticationFailure(childEx, authReq); + verifyNoMoreInteractions(publisher); + } + + private AuthenticationProvider createProviderWhichDoesNotSupport() { + AuthenticationProvider provider = mock(AuthenticationProvider.class); + given(provider.supports(any(Class.class))).willReturn(false); + return provider; + } + private AuthenticationProvider createProviderWhichThrows(final AuthenticationException ex) { AuthenticationProvider provider = mock(AuthenticationProvider.class); given(provider.supports(any(Class.class))).willReturn(true);