diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java index 2fdc2d48c4..399e2aede5 100644 --- a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.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. @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.util.Assert; /** @@ -58,6 +59,7 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti public Mono authenticate(Authentication authentication) { Flux result = Flux.fromIterable(this.delegates); Function> logging = (m) -> m.authenticate(authentication) + .doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication)) .doOnError(this.logger::debug); return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next(); 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 479f99f704..aa8b82bcd7 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -202,6 +202,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar throw ex; } catch (AuthenticationException ex) { + ex.setAuthenticationRequest(authentication); logger.debug(LogMessage.format("Authentication failed with provider %s since %s", provider.getClass().getSimpleName(), ex.getMessage())); lastException = ex; @@ -265,6 +266,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar @SuppressWarnings("deprecation") private void prepareException(AuthenticationException ex, Authentication auth) { + ex.setAuthenticationRequest(auth); this.eventPublisher.publishAuthenticationFailure(ex, auth); } diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java index 2d4b2c7a15..6d7aa59018 100644 --- a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.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. @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -108,6 +109,15 @@ public class DelegatingReactiveAuthenticationManagerTests { assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication); } + @Test + void whenAccountStatusExceptionThenAuthenticationRequestIsIncluded() { + AuthenticationException expected = new LockedException(""); + given(this.delegate1.authenticate(any())).willReturn(Mono.error(expected)); + ReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1); + StepVerifier.create(manager.authenticate(this.authentication)).expectError(LockedException.class).verify(); + assertThat(expected.getAuthenticationRequest()).isEqualTo(this.authentication); + } + private DelegatingReactiveAuthenticationManager managerWithContinueOnError() { DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); 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 3bdf852558..7bb0c136bc 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -253,6 +253,34 @@ public class ProviderManagerTests { verify(publisher).publishAuthenticationFailure(expected, authReq); } + @Test + void whenAccountStatusExceptionThenAuthenticationRequestIsIncluded() { + AuthenticationException expected = new LockedException(""); + ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected)); + Authentication authReq = mock(Authentication.class); + assertThatExceptionOfType(LockedException.class).isThrownBy(() -> mgr.authenticate(authReq)); + assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq); + } + + @Test + void whenInternalServiceAuthenticationExceptionThenAuthenticationRequestIsIncluded() { + AuthenticationException expected = new InternalAuthenticationServiceException(""); + ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected)); + Authentication authReq = mock(Authentication.class); + assertThatExceptionOfType(InternalAuthenticationServiceException.class) + .isThrownBy(() -> mgr.authenticate(authReq)); + assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq); + } + + @Test + void whenAuthenticationExceptionThenAuthenticationRequestIsIncluded() { + AuthenticationException expected = new BadCredentialsException(""); + ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected)); + Authentication authReq = mock(Authentication.class); + assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> mgr.authenticate(authReq)); + assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq); + } + // SEC-2367 @Test void providerThrowsInternalAuthenticationServiceException() { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java index 5d80e981bb..de1bda32a7 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -176,9 +176,17 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat String issuer = this.issuerConverter.convert(token); AuthenticationManager authenticationManager = this.issuerAuthenticationManagerResolver.resolve(issuer); if (authenticationManager == null) { - throw new InvalidBearerTokenException("Invalid issuer"); + AuthenticationException ex = new InvalidBearerTokenException("Invalid issuer"); + ex.setAuthenticationRequest(authentication); + throw ex; + } + try { + return authenticationManager.authenticate(authentication); + } + catch (AuthenticationException ex) { + ex.setAuthenticationRequest(authentication); + throw ex; } - return authenticationManager.authenticate(authentication); } } @@ -194,10 +202,14 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat return issuer; } } - catch (Exception ex) { - throw new InvalidBearerTokenException(ex.getMessage(), ex); + catch (Exception cause) { + AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause); + ex.setAuthenticationRequest(authentication); + throw ex; } - throw new InvalidBearerTokenException("Missing issuer"); + AuthenticationException ex = new InvalidBearerTokenException("Missing issuer"); + ex.setAuthenticationRequest(authentication); + throw ex; } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index 2e81d3b3d8..b764e4ca76 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; @@ -181,8 +182,13 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication; return this.issuerConverter.convert(token) .flatMap((issuer) -> this.issuerAuthenticationManagerResolver.resolve(issuer) - .switchIfEmpty(Mono.error(() -> new InvalidBearerTokenException("Invalid issuer " + issuer)))) - .flatMap((manager) -> manager.authenticate(authentication)); + .switchIfEmpty(Mono.error(() -> { + AuthenticationException ex = new InvalidBearerTokenException("Invalid issuer " + issuer); + ex.setAuthenticationRequest(authentication); + return ex; + }))) + .flatMap((manager) -> manager.authenticate(authentication)) + .doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication)); } } @@ -194,12 +200,18 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver try { String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer(); if (issuer == null) { - throw new InvalidBearerTokenException("Missing issuer"); + AuthenticationException ex = new InvalidBearerTokenException("Missing issuer"); + ex.setAuthenticationRequest(token); + throw ex; } return Mono.just(issuer); } - catch (Exception ex) { - return Mono.error(() -> new InvalidBearerTokenException(ex.getMessage(), ex)); + catch (Exception cause) { + return Mono.error(() -> { + AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause); + ex.setAuthenticationRequest(token); + return ex; + }); } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java index 8a50dab153..8d5e9c7780 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -37,14 +37,18 @@ import org.junit.jupiter.api.Test; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.verify; @@ -263,6 +267,19 @@ public class JwtIssuerAuthenticationManagerResolverTests { // @formatter:on } + @Test + public void resolveWhenAuthenticationExceptionThenAuthenticationRequestIsIncluded() { + Authentication authentication = new BearerTokenAuthenticationToken(this.jwt); + AuthenticationException ex = new InvalidBearerTokenException(""); + AuthenticationManager manager = mock(AuthenticationManager.class); + given(manager.authenticate(any())).willThrow(ex); + JwtIssuerAuthenticationManagerResolver resolver = new JwtIssuerAuthenticationManagerResolver( + (issuer) -> manager); + assertThatExceptionOfType(InvalidBearerTokenException.class) + .isThrownBy(() -> resolver.resolve(null).authenticate(authentication)); + assertThat(ex.getAuthenticationRequest()).isEqualTo(authentication); + } + @Test public void factoryWhenNullOrEmptyIssuersThenException() { assertThatIllegalArgumentException() diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java index f12c6d65be..63bd66cf73 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -34,13 +34,16 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver; import static org.assertj.core.api.Assertions.assertThat; @@ -262,6 +265,20 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { // @formatter:on } + @Test + public void resolveWhenAuthenticationExceptionThenAuthenticationRequestIsIncluded() { + Authentication authentication = new BearerTokenAuthenticationToken(this.jwt); + AuthenticationException ex = new InvalidBearerTokenException(""); + ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class); + given(manager.authenticate(any())).willReturn(Mono.error(ex)); + JwtIssuerReactiveAuthenticationManagerResolver resolver = new JwtIssuerReactiveAuthenticationManagerResolver( + (issuer) -> Mono.just(manager)); + StepVerifier.create(resolver.resolve(null).block().authenticate(authentication)) + .expectError(InvalidBearerTokenException.class) + .verify(); + assertThat(ex.getAuthenticationRequest()).isEqualTo(authentication); + } + @Test public void factoryWhenNullOrEmptyIssuersThenException() { assertThatIllegalArgumentException().isThrownBy( diff --git a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java index 9a38b1d231..765fec6ed3 100644 --- a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2004-2022 the original author or authors. + * Copyright 2004-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. @@ -194,10 +194,11 @@ public class ExceptionTranslationFilter extends GenericFilterBean implements Mes logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception); } - sendStartAuthentication(request, response, chain, - new InsufficientAuthenticationException( - this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", - "Full authentication is required to access this resource"))); + AuthenticationException ex = new InsufficientAuthenticationException( + this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", + "Full authentication is required to access this resource")); + ex.setAuthenticationRequest(authentication); + sendStartAuthentication(request, response, chain, ex); } else { if (logger.isTraceEnabled()) { diff --git a/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java index 04833fdeae..eb164ea9e8 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java +++ b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -27,6 +27,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; @@ -46,7 +47,9 @@ public final class RequestMatcherDelegatingAuthenticationManagerResolver private final List> authenticationManagers; private AuthenticationManager defaultAuthenticationManager = (authentication) -> { - throw new AuthenticationServiceException("Cannot authenticate " + authentication); + AuthenticationException ex = new AuthenticationServiceException("Cannot authenticate " + authentication); + ex.setAuthenticationRequest(authentication); + throw ex; }; /** diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java index 4605b0d199..efca0acb62 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.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. @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; @@ -46,8 +47,11 @@ public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResol private final List> authenticationManagers; - private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono - .error(new AuthenticationServiceException("Cannot authenticate " + authentication)); + private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> { + AuthenticationException ex = new AuthenticationServiceException("Cannot authenticate " + authentication); + ex.setAuthenticationRequest(authentication); + return Mono.error(ex); + }; /** * Construct an diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java index 01d990f177..0c85e6ef03 100644 --- a/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java @@ -101,6 +101,9 @@ public class ExceptionTranslationWebFilter implements WebFilter { AuthenticationException cause = new InsufficientAuthenticationException( "Full authentication is required to access this resource"); AuthenticationException ex = new AuthenticationCredentialsNotFoundException("Not Authenticated", cause); + if (authentication != null) { + ex.setAuthenticationRequest(authentication); + } return this.authenticationEntryPoint.commence(exchange, ex).then(Mono.empty()); } diff --git a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java index 085ec95578..89159fd737 100644 --- a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2004-2024 the original author or authors. + * Copyright 2004-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. @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpSession; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; @@ -38,6 +39,7 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -107,6 +109,23 @@ public class ExceptionTranslationFilterTests { assertThat(response.getRedirectedUrl()).isEqualTo("/mycontext/login.jsp"); } + @Test + public void testAccessDeniedWhenAnonymousThenIncludesAuthenticationRequest() throws Exception { + // Setup our HTTP request + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + FilterChain fc = mockFilterChainWithException(new AccessDeniedException("")); + AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("ignored", "ignored", + AuthorityUtils.createAuthorityList("IGNORED")); + SecurityContextHolder.getContext().setAuthentication(token); + AuthenticationEntryPoint entryPoint = mock(AuthenticationEntryPoint.class); + ExceptionTranslationFilter filter = new ExceptionTranslationFilter(entryPoint); + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, fc); + ArgumentCaptor ex = ArgumentCaptor.forClass(AuthenticationException.class); + verify(entryPoint).commence(any(), any(), ex.capture()); + assertThat(ex.getValue().getAuthenticationRequest()).isEqualTo(token); + } + @Test public void testAccessDeniedWithRememberMe() throws Exception { // Setup our HTTP request diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java index baed5b52d1..0c4f2a2177 100644 --- a/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilterTests.java @@ -21,6 +21,7 @@ import java.security.Principal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; @@ -31,6 +32,7 @@ import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; @@ -39,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; /** * @author Rob Winch @@ -146,6 +149,17 @@ public class ExceptionTranslationWebFilterTests { this.entryPointPublisher.assertWasSubscribed(); } + @Test + public void filterWhenAccessDeniedExceptionAndAnonymousAuthenticatedThenIncludesAuthenticationRequest() { + given(this.entryPoint.commence(any(), any())).willReturn(this.entryPointPublisher.mono()); + given(this.exchange.getPrincipal()).willReturn(Mono.just(this.anonymousPrincipal)); + given(this.chain.filter(this.exchange)).willReturn(Mono.error(new AccessDeniedException("Not Authorized"))); + StepVerifier.create(this.filter.filter(this.exchange, this.chain)).expectComplete().verify(); + ArgumentCaptor ex = ArgumentCaptor.forClass(AuthenticationException.class); + verify(this.entryPoint).commence(any(), ex.capture()); + assertThat(ex.getValue().getAuthenticationRequest()).isEqualTo(this.anonymousPrincipal); + } + @Test public void setAccessDeniedHandlerWhenNullThenException() { assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAccessDeniedHandler(null));