Provide Authentication to AuthenticationExceptions

Issue gh-16444
This commit is contained in:
Josh Cummings 2025-03-21 17:45:10 -06:00
parent 464e506429
commit 56e757a2a1
14 changed files with 172 additions and 28 deletions

View File

@ -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<Authentication> authenticate(Authentication authentication) {
Flux<ReactiveAuthenticationManager> result = Flux.fromIterable(this.delegates);
Function<ReactiveAuthenticationManager, Mono<Authentication>> 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();

View File

@ -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);
}

View File

@ -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);

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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;
});
}
}

View File

@ -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()

View File

@ -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(

View File

@ -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()) {

View File

@ -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<RequestMatcherEntry<AuthenticationManager>> authenticationManagers;
private AuthenticationManager defaultAuthenticationManager = (authentication) -> {
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
AuthenticationException ex = new AuthenticationServiceException("Cannot authenticate " + authentication);
ex.setAuthenticationRequest(authentication);
throw ex;
};
/**

View File

@ -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<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> 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

View File

@ -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());
}

View File

@ -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<AuthenticationException> 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

View File

@ -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<AuthenticationException> 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));