From c58ff11e80c5fcfd92c6b00fd7aafc16c8c2d3d6 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:12:25 -0600 Subject: [PATCH 1/5] Polish Tests Issue gh-16444 --- .../authentication/ProviderManagerTests.java | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) 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 9b98bd522a9..3bdf8525585 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -47,7 +46,7 @@ public class ProviderManagerTests { @Test - public void authenticationFailsWithUnsupportedToken() { + void authenticationFailsWithUnsupportedToken() { Authentication token = new AbstractAuthenticationToken(null) { @Override public Object getCredentials() { @@ -65,7 +64,7 @@ public Object getPrincipal() { } @Test - public void credentialsAreClearedByDefault() { + void credentialsAreClearedByDefault() { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("Test", "Password"); ProviderManager mgr = makeProviderManager(); @@ -78,8 +77,8 @@ public void credentialsAreClearedByDefault() { } @Test - public void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() { - final Authentication a = mock(Authentication.class); + void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() { + Authentication a = mock(Authentication.class); ProviderManager mgr = new ProviderManager(createProviderWhichReturns(a)); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); @@ -89,8 +88,8 @@ public void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() { } @Test - public void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() { - final Authentication a = mock(Authentication.class); + void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() { + Authentication a = mock(Authentication.class); ProviderManager mgr = new ProviderManager( Arrays.asList(createProviderWhichReturns(null), createProviderWhichReturns(a))); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); @@ -101,24 +100,24 @@ public void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthentic } @Test - public void testStartupFailsIfProvidersNotSetAsList() { + void testStartupFailsIfProvidersNotSetAsList() { assertThatIllegalArgumentException().isThrownBy(() -> new ProviderManager((List) null)); } @Test - public void testStartupFailsIfProvidersNotSetAsVarargs() { + void testStartupFailsIfProvidersNotSetAsVarargs() { assertThatIllegalArgumentException().isThrownBy(() -> new ProviderManager((AuthenticationProvider) null)); } @Test - public void testStartupFailsIfProvidersContainNullElement() { + void testStartupFailsIfProvidersContainNullElement() { assertThatIllegalArgumentException() .isThrownBy(() -> new ProviderManager(Arrays.asList(mock(AuthenticationProvider.class), null))); } // gh-8689 @Test - public void constructorWhenUsingListOfThenNoException() { + void constructorWhenUsingListOfThenNoException() { List providers = spy(ArrayList.class); // List.of(null) in JDK 9 throws a NullPointerException given(providers.contains(eq(null))).willThrow(NullPointerException.class); @@ -127,7 +126,7 @@ public void constructorWhenUsingListOfThenNoException() { } @Test - public void detailsAreNotSetOnAuthenticationTokenIfAlreadySetByProvider() { + void detailsAreNotSetOnAuthenticationTokenIfAlreadySetByProvider() { Object requestDetails = "(Request Details)"; final Object resultDetails = "(Result Details)"; // A provider which sets the details object @@ -151,7 +150,7 @@ public boolean supports(Class authentication) { } @Test - public void detailsAreSetOnAuthenticationTokenIfNotAlreadySetByProvider() { + void detailsAreSetOnAuthenticationTokenIfNotAlreadySetByProvider() { Object details = new Object(); ProviderManager authMgr = makeProviderManager(); TestingAuthenticationToken request = createAuthenticationToken(); @@ -162,8 +161,8 @@ public void detailsAreSetOnAuthenticationTokenIfNotAlreadySetByProvider() { } @Test - public void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() { - final Authentication authReq = mock(Authentication.class); + void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() { + Authentication authReq = mock(Authentication.class); ProviderManager mgr = new ProviderManager( createProviderWhichThrows(new BadCredentialsException("", new Throwable())), createProviderWhichReturns(authReq)); @@ -171,7 +170,7 @@ public void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() { } @Test - public void authenticationExceptionIsRethrownIfNoLaterProviderAuthenticates() { + void authenticationExceptionIsRethrownIfNoLaterProviderAuthenticates() { ProviderManager mgr = new ProviderManager(Arrays .asList(createProviderWhichThrows(new BadCredentialsException("")), createProviderWhichReturns(null))); assertThatExceptionOfType(BadCredentialsException.class) @@ -180,7 +179,7 @@ public void authenticationExceptionIsRethrownIfNoLaterProviderAuthenticates() { // SEC-546 @Test - public void accountStatusExceptionPreventsCallsToSubsequentProviders() { + void accountStatusExceptionPreventsCallsToSubsequentProviders() { AuthenticationProvider iThrowAccountStatusException = createProviderWhichThrows(new AccountStatusException("") { }); AuthenticationProvider otherProvider = mock(AuthenticationProvider.class); @@ -191,48 +190,47 @@ public void accountStatusExceptionPreventsCallsToSubsequentProviders() { } @Test - public void parentAuthenticationIsUsedIfProvidersDontAuthenticate() { + void parentAuthenticationIsUsedIfProvidersDontAuthenticate() { AuthenticationManager parent = mock(AuthenticationManager.class); Authentication authReq = mock(Authentication.class); given(parent.authenticate(authReq)).willReturn(authReq); - ProviderManager mgr = new ProviderManager(Collections.singletonList(mock(AuthenticationProvider.class)), - parent); + ProviderManager mgr = new ProviderManager(List.of(mock(AuthenticationProvider.class)), parent); assertThat(mgr.authenticate(authReq)).isSameAs(authReq); } @Test - public void parentIsNotCalledIfAccountStatusExceptionIsThrown() { + void parentIsNotCalledIfAccountStatusExceptionIsThrown() { AuthenticationProvider iThrowAccountStatusException = createProviderWhichThrows( new AccountStatusException("", new Throwable()) { }); AuthenticationManager parent = mock(AuthenticationManager.class); - ProviderManager mgr = new ProviderManager(Collections.singletonList(iThrowAccountStatusException), parent); + ProviderManager mgr = new ProviderManager(List.of(iThrowAccountStatusException), parent); assertThatExceptionOfType(AccountStatusException.class) .isThrownBy(() -> mgr.authenticate(mock(Authentication.class))); verifyNoInteractions(parent); } @Test - public void providerNotFoundFromParentIsIgnored() { + void providerNotFoundFromParentIsIgnored() { final Authentication authReq = mock(Authentication.class); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); AuthenticationManager parent = mock(AuthenticationManager.class); given(parent.authenticate(authReq)).willThrow(new ProviderNotFoundException("")); // Set a provider that throws an exception - this is the exception we expect to be // propagated - ProviderManager mgr = new ProviderManager( - Collections.singletonList(createProviderWhichThrows(new BadCredentialsException(""))), parent); + ProviderManager mgr = new ProviderManager(List.of(createProviderWhichThrows(new BadCredentialsException(""))), + parent); mgr.setAuthenticationEventPublisher(publisher); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> mgr.authenticate(authReq)) .satisfies((ex) -> verify(publisher).publishAuthenticationFailure(ex, authReq)); } @Test - public void authenticationExceptionFromParentOverridesPreviousOnes() { + void authenticationExceptionFromParentOverridesPreviousOnes() { AuthenticationManager parent = mock(AuthenticationManager.class); - ProviderManager mgr = new ProviderManager( - Collections.singletonList(createProviderWhichThrows(new BadCredentialsException(""))), parent); - final Authentication authReq = mock(Authentication.class); + ProviderManager mgr = new ProviderManager(List.of(createProviderWhichThrows(new BadCredentialsException(""))), + parent); + Authentication authReq = mock(Authentication.class); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); // Set a provider that throws an exception - this is the exception we expect to be @@ -244,12 +242,11 @@ public void authenticationExceptionFromParentOverridesPreviousOnes() { } @Test - public void statusExceptionIsPublished() { + void statusExceptionIsPublished() { AuthenticationManager parent = mock(AuthenticationManager.class); - final LockedException expected = new LockedException(""); - ProviderManager mgr = new ProviderManager(Collections.singletonList(createProviderWhichThrows(expected)), - parent); - final Authentication authReq = mock(Authentication.class); + LockedException expected = new LockedException(""); + ProviderManager mgr = new ProviderManager(List.of(createProviderWhichThrows(expected)), parent); + Authentication authReq = mock(Authentication.class); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); assertThatExceptionOfType(LockedException.class).isThrownBy(() -> mgr.authenticate(authReq)); @@ -258,7 +255,7 @@ public void statusExceptionIsPublished() { // SEC-2367 @Test - public void providerThrowsInternalAuthenticationServiceException() { + void providerThrowsInternalAuthenticationServiceException() { InternalAuthenticationServiceException expected = new InternalAuthenticationServiceException("Expected"); ProviderManager mgr = new ProviderManager(Arrays.asList(createProviderWhichThrows(expected), createProviderWhichThrows(new BadCredentialsException("Oops"))), null); @@ -269,15 +266,15 @@ public void providerThrowsInternalAuthenticationServiceException() { // gh-6281 @Test - public void authenticateWhenFailsInParentAndPublishesThenChildDoesNotPublish() { + void authenticateWhenFailsInParentAndPublishesThenChildDoesNotPublish() { BadCredentialsException badCredentialsExParent = new BadCredentialsException("Bad Credentials in parent"); ProviderManager parentMgr = new ProviderManager(createProviderWhichThrows(badCredentialsExParent)); - ProviderManager childMgr = new ProviderManager(Collections.singletonList( - createProviderWhichThrows(new BadCredentialsException("Bad Credentials in child"))), parentMgr); + ProviderManager childMgr = new ProviderManager( + List.of(createProviderWhichThrows(new BadCredentialsException("Bad Credentials in child"))), parentMgr); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); parentMgr.setAuthenticationEventPublisher(publisher); childMgr.setAuthenticationEventPublisher(publisher); - final Authentication authReq = mock(Authentication.class); + Authentication authReq = mock(Authentication.class); assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> childMgr.authenticate(authReq)) .isSameAs(badCredentialsExParent); verify(publisher).publishAuthenticationFailure(badCredentialsExParent, authReq); // Parent From 8018802c5b01831ca74d9ed237df1050484f54db Mon Sep 17 00:00:00 2001 From: amm0124 Date: Thu, 30 Jan 2025 17:05:52 +0900 Subject: [PATCH 2/5] Add authRequest field to AuthenticationException Store the authentication request details in the `authRequest` field of `AuthenticationException` when an authentication exception occurs. Closes gh-16444 Signed-off-by: amm0124 --- .../core/AuthenticationException.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/src/main/java/org/springframework/security/core/AuthenticationException.java b/core/src/main/java/org/springframework/security/core/AuthenticationException.java index 9e1fb756086..914f165dca5 100644 --- a/core/src/main/java/org/springframework/security/core/AuthenticationException.java +++ b/core/src/main/java/org/springframework/security/core/AuthenticationException.java @@ -18,6 +18,8 @@ import java.io.Serial; +import org.springframework.util.Assert; + /** * Abstract superclass for all exceptions related to an {@link Authentication} object * being invalid for whatever reason. @@ -29,6 +31,16 @@ public abstract class AuthenticationException extends RuntimeException { @Serial private static final long serialVersionUID = 2018827803361503060L; + /** + * The {@link Authentication} object representing the failed authentication attempt. + *

+ * This field captures the authentication request that was attempted but ultimately + * failed, providing critical information for diagnosing the failure and facilitating + * debugging. If set, the value must not be null. + *

+ */ + private Authentication authRequest; + /** * Constructs an {@code AuthenticationException} with the specified message and root * cause. @@ -37,6 +49,7 @@ public abstract class AuthenticationException extends RuntimeException { */ public AuthenticationException(String msg, Throwable cause) { super(msg, cause); + this.authRequest = null; } /** @@ -46,6 +59,23 @@ public AuthenticationException(String msg, Throwable cause) { */ public AuthenticationException(String msg) { super(msg); + this.authRequest = null; + } + + + /** + * Sets the {@link Authentication} object representing the failed authentication + * attempt. + *

+ * This method allows the injection of the authentication request that resulted in a + * failure. The provided {@code authRequest} should not be null if set. + *

+ * @param authRequest the authentication request associated with the failed + * authentication attempt. + */ + public void setAuthRequest(Authentication authRequest) { + Assert.notNull(authRequest, "AuthRequest cannot be null"); + this.authRequest = authRequest; } } From cdb485f5e99bd060612c6eaadb9501b649aa3112 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:26:50 -0600 Subject: [PATCH 3/5] Polish AuthenticationRequest Property - Add getter for reading the request - Update BadCredentialsMixing to ignore authentication - Allow exception to be mutable Issue gh-16444 --- .../core/AuthenticationException.java | 41 ++++++++++--------- .../BadCredentialsExceptionMixin.java | 2 +- etc/checkstyle/checkstyle-suppressions.xml | 1 + 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/org/springframework/security/core/AuthenticationException.java b/core/src/main/java/org/springframework/security/core/AuthenticationException.java index 914f165dca5..8efe1be55f1 100644 --- a/core/src/main/java/org/springframework/security/core/AuthenticationException.java +++ b/core/src/main/java/org/springframework/security/core/AuthenticationException.java @@ -31,15 +31,7 @@ public abstract class AuthenticationException extends RuntimeException { @Serial private static final long serialVersionUID = 2018827803361503060L; - /** - * The {@link Authentication} object representing the failed authentication attempt. - *

- * This field captures the authentication request that was attempted but ultimately - * failed, providing critical information for diagnosing the failure and facilitating - * debugging. If set, the value must not be null. - *

- */ - private Authentication authRequest; + private Authentication authenticationRequest; /** * Constructs an {@code AuthenticationException} with the specified message and root @@ -49,7 +41,6 @@ public abstract class AuthenticationException extends RuntimeException { */ public AuthenticationException(String msg, Throwable cause) { super(msg, cause); - this.authRequest = null; } /** @@ -59,23 +50,33 @@ public AuthenticationException(String msg, Throwable cause) { */ public AuthenticationException(String msg) { super(msg); - this.authRequest = null; } + /** + * Get the {@link Authentication} object representing the failed authentication + * attempt. + *

+ * This field captures the authentication request that was attempted but ultimately + * failed, providing critical information for diagnosing the failure and facilitating + * debugging + * @since 6.5 + */ + public Authentication getAuthenticationRequest() { + return this.authenticationRequest; + } /** - * Sets the {@link Authentication} object representing the failed authentication + * Set the {@link Authentication} object representing the failed authentication * attempt. *

- * This method allows the injection of the authentication request that resulted in a - * failure. The provided {@code authRequest} should not be null if set. - *

- * @param authRequest the authentication request associated with the failed - * authentication attempt. + * The provided {@code authenticationRequest} should not be null + * @param authenticationRequest the authentication request associated with the failed + * authentication attempt + * @since 6.5 */ - public void setAuthRequest(Authentication authRequest) { - Assert.notNull(authRequest, "AuthRequest cannot be null"); - this.authRequest = authRequest; + public void setAuthenticationRequest(Authentication authenticationRequest) { + Assert.notNull(authenticationRequest, "authenticationRequest cannot be null"); + this.authenticationRequest = authenticationRequest; } } diff --git a/core/src/main/java/org/springframework/security/jackson2/BadCredentialsExceptionMixin.java b/core/src/main/java/org/springframework/security/jackson2/BadCredentialsExceptionMixin.java index 5471374b4d7..aedb7507adf 100644 --- a/core/src/main/java/org/springframework/security/jackson2/BadCredentialsExceptionMixin.java +++ b/core/src/main/java/org/springframework/security/jackson2/BadCredentialsExceptionMixin.java @@ -40,7 +40,7 @@ * @see CoreJackson2Module */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) -@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace" }) +@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "authenticationRequest" }) class BadCredentialsExceptionMixin { /** diff --git a/etc/checkstyle/checkstyle-suppressions.xml b/etc/checkstyle/checkstyle-suppressions.xml index b368ce84e84..c1c5baf08a3 100644 --- a/etc/checkstyle/checkstyle-suppressions.xml +++ b/etc/checkstyle/checkstyle-suppressions.xml @@ -38,6 +38,7 @@ + From 640d09a3b8a274a96152bdddf6b0ac91cbd2755f Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:43:39 -0600 Subject: [PATCH 4/5] Polish ExceptionTranslateWebFilter - Isolated exception construction - Isolated entry point subscription Issue gh-16444 --- .../ExceptionTranslationWebFilter.java | 32 +++++++++++-------- .../ExceptionTranslationWebFilterTests.java | 3 +- 2 files changed, 20 insertions(+), 15 deletions(-) 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 6be2a6258ea..01d990f177d 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 @@ -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. @@ -50,14 +50,19 @@ public class ExceptionTranslationWebFilter implements WebFilter { @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange) - .onErrorResume(AccessDeniedException.class, (denied) -> exchange.getPrincipal() - .filter((principal) -> (!(principal instanceof Authentication) || (principal instanceof Authentication - && (this.authenticationTrustResolver.isAuthenticated((Authentication) principal))))) - .switchIfEmpty(commenceAuthentication(exchange, - new InsufficientAuthenticationException( - "Full authentication is required to access this resource"))) - .flatMap((principal) -> this.accessDeniedHandler.handle(exchange, denied)) - .then()); + .onErrorResume(AccessDeniedException.class, + (denied) -> exchange.getPrincipal() + .switchIfEmpty(Mono.defer(() -> commenceAuthentication(exchange, null))) + .flatMap((principal) -> { + if (!(principal instanceof Authentication authentication)) { + return this.accessDeniedHandler.handle(exchange, denied); + } + if (this.authenticationTrustResolver.isAuthenticated(authentication)) { + return this.accessDeniedHandler.handle(exchange, denied); + } + return commenceAuthentication(exchange, authentication); + }) + .then()); } /** @@ -92,10 +97,11 @@ public void setAuthenticationTrustResolver(AuthenticationTrustResolver authentic this.authenticationTrustResolver = authenticationTrustResolver; } - private Mono commenceAuthentication(ServerWebExchange exchange, AuthenticationException denied) { - return this.authenticationEntryPoint - .commence(exchange, new AuthenticationCredentialsNotFoundException("Not Authenticated", denied)) - .then(Mono.empty()); + private Mono commenceAuthentication(ServerWebExchange exchange, Authentication authentication) { + AuthenticationException cause = new InsufficientAuthenticationException( + "Full authentication is required to access this resource"); + AuthenticationException ex = new AuthenticationCredentialsNotFoundException("Not Authenticated", cause); + return this.authenticationEntryPoint.commence(exchange, ex).then(Mono.empty()); } } 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 e323572b1af..baed5b52d12 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 @@ -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. @@ -129,7 +129,6 @@ public void filterWhenDefaultsAndAccessDeniedExceptionAndNotAuthenticatedThenUna @Test public void filterWhenAccessDeniedExceptionAndAuthenticatedThenHandled() { given(this.deniedHandler.handle(any(), any())).willReturn(this.deniedPublisher.mono()); - given(this.entryPoint.commence(any(), any())).willReturn(this.entryPointPublisher.mono()); given(this.exchange.getPrincipal()).willReturn(Mono.just(this.principal)); given(this.chain.filter(this.exchange)).willReturn(Mono.error(new AccessDeniedException("Not Authorized"))); StepVerifier.create(this.filter.filter(this.exchange, this.chain)).expectComplete().verify(); From 659e7d25f49963ac4b2f6f104a8a69032def89d6 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:45:10 -0600 Subject: [PATCH 5/5] Provide Authentication to AuthenticationExceptions Issue gh-16444 --- ...legatingReactiveAuthenticationManager.java | 4 ++- .../authentication/ProviderManager.java | 4 ++- ...ingReactiveAuthenticationManagerTests.java | 12 +++++++- .../authentication/ProviderManagerTests.java | 28 +++++++++++++++++++ ...wtIssuerAuthenticationManagerResolver.java | 24 ++++++++++++---- ...ReactiveAuthenticationManagerResolver.java | 24 ++++++++++++---- ...uerAuthenticationManagerResolverTests.java | 19 ++++++++++++- ...iveAuthenticationManagerResolverTests.java | 19 ++++++++++++- .../access/ExceptionTranslationFilter.java | 11 ++++---- ...legatingAuthenticationManagerResolver.java | 7 +++-- ...ReactiveAuthenticationManagerResolver.java | 10 +++++-- .../ExceptionTranslationWebFilter.java | 3 ++ .../ExceptionTranslationFilterTests.java | 21 +++++++++++++- .../ExceptionTranslationWebFilterTests.java | 14 ++++++++++ 14 files changed, 172 insertions(+), 28 deletions(-) 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 2fdc2d48c42..399e2aede59 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.Mono; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.util.Assert; /** @@ -58,6 +59,7 @@ public DelegatingReactiveAuthenticationManager(List 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 479f99f704e..aa8b82bcd71 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 Authentication authenticate(Authentication authentication) throws Authent 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 Authentication authenticate(Authentication authentication) throws Authent @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 2d4b2c7a158..6d7aa590184 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.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 void authenticateWhenContinueOnErrorAndDelegate1NotEmptyThenReturnsNotEmp 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 3bdf8525585..7bb0c136bca 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 @@ void statusExceptionIsPublished() { 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 5d80e981bb0..de1bda32a7c 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 Authentication authenticate(Authentication authentication) throws Authent 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 String convert(@NonNull BearerTokenAuthenticationToken authentication) { 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 2e81d3b3d8b..b764e4ca76c 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.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 Mono authenticate(Authentication authentication) { 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 Mono convert(@NonNull BearerTokenAuthenticationToken token) { 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 8a50dab1530..8d5e9c7780e 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.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 void resolveWhenBearerTokenEvilThenGenericException() { // @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 f12c6d65be5..63bd66cf735 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.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 void resolveWhenBearerTokenEvilThenGenericException() { // @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 9a38b1d2312..765fec6ed3d 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 @@ private void handleAccessDeniedException(HttpServletRequest request, HttpServlet 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 04833fdeae5..eb164ea9e89 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.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 4605b0d1993..efca0acb628 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 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 01d990f177d..0c85e6ef033 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 @@ private Mono commenceAuthentication(ServerWebExchange exchange, Authentic 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 085ec955780..89159fd7377 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 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.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 void testAccessDeniedWhenAnonymous() throws Exception { 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 baed5b52d12..0c4f2a21775 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 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.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.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 void filterWhenAccessDeniedExceptionAndAnonymousAuthenticatedThenHandled( 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));