Skip to content

Commit 659e7d2

Browse files
committed
Provide Authentication to AuthenticationExceptions
Issue gh-16444
1 parent 640d09a commit 659e7d2

File tree

14 files changed

+172
-28
lines changed

14 files changed

+172
-28
lines changed

core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Mono;
2727

2828
import org.springframework.security.core.Authentication;
29+
import org.springframework.security.core.AuthenticationException;
2930
import org.springframework.util.Assert;
3031

3132
/**
@@ -58,6 +59,7 @@ public DelegatingReactiveAuthenticationManager(List<ReactiveAuthenticationManage
5859
public Mono<Authentication> authenticate(Authentication authentication) {
5960
Flux<ReactiveAuthenticationManager> result = Flux.fromIterable(this.delegates);
6061
Function<ReactiveAuthenticationManager, Mono<Authentication>> logging = (m) -> m.authenticate(authentication)
62+
.doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication))
6163
.doOnError(this.logger::debug);
6264

6365
return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();

core/src/main/java/org/springframework/security/authentication/ProviderManager.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -202,6 +202,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
202202
throw ex;
203203
}
204204
catch (AuthenticationException ex) {
205+
ex.setAuthenticationRequest(authentication);
205206
logger.debug(LogMessage.format("Authentication failed with provider %s since %s",
206207
provider.getClass().getSimpleName(), ex.getMessage()));
207208
lastException = ex;
@@ -265,6 +266,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
265266

266267
@SuppressWarnings("deprecation")
267268
private void prepareException(AuthenticationException ex, Authentication auth) {
269+
ex.setAuthenticationRequest(auth);
268270
this.eventPublisher.publishAuthenticationFailure(ex, auth);
269271
}
270272

core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import reactor.test.StepVerifier;
2727

2828
import org.springframework.security.core.Authentication;
29+
import org.springframework.security.core.AuthenticationException;
2930

3031
import static org.assertj.core.api.Assertions.assertThat;
3132
import static org.mockito.ArgumentMatchers.any;
@@ -108,6 +109,15 @@ public void authenticateWhenContinueOnErrorAndDelegate1NotEmptyThenReturnsNotEmp
108109
assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication);
109110
}
110111

112+
@Test
113+
void whenAccountStatusExceptionThenAuthenticationRequestIsIncluded() {
114+
AuthenticationException expected = new LockedException("");
115+
given(this.delegate1.authenticate(any())).willReturn(Mono.error(expected));
116+
ReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1);
117+
StepVerifier.create(manager.authenticate(this.authentication)).expectError(LockedException.class).verify();
118+
assertThat(expected.getAuthenticationRequest()).isEqualTo(this.authentication);
119+
}
120+
111121
private DelegatingReactiveAuthenticationManager managerWithContinueOnError() {
112122
DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1,
113123
this.delegate2);

core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java

+28
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,34 @@ void statusExceptionIsPublished() {
253253
verify(publisher).publishAuthenticationFailure(expected, authReq);
254254
}
255255

256+
@Test
257+
void whenAccountStatusExceptionThenAuthenticationRequestIsIncluded() {
258+
AuthenticationException expected = new LockedException("");
259+
ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected));
260+
Authentication authReq = mock(Authentication.class);
261+
assertThatExceptionOfType(LockedException.class).isThrownBy(() -> mgr.authenticate(authReq));
262+
assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq);
263+
}
264+
265+
@Test
266+
void whenInternalServiceAuthenticationExceptionThenAuthenticationRequestIsIncluded() {
267+
AuthenticationException expected = new InternalAuthenticationServiceException("");
268+
ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected));
269+
Authentication authReq = mock(Authentication.class);
270+
assertThatExceptionOfType(InternalAuthenticationServiceException.class)
271+
.isThrownBy(() -> mgr.authenticate(authReq));
272+
assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq);
273+
}
274+
275+
@Test
276+
void whenAuthenticationExceptionThenAuthenticationRequestIsIncluded() {
277+
AuthenticationException expected = new BadCredentialsException("");
278+
ProviderManager mgr = new ProviderManager(createProviderWhichThrows(expected));
279+
Authentication authReq = mock(Authentication.class);
280+
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> mgr.authenticate(authReq));
281+
assertThat(expected.getAuthenticationRequest()).isEqualTo(authReq);
282+
}
283+
256284
// SEC-2367
257285
@Test
258286
void providerThrowsInternalAuthenticationServiceException() {

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -176,9 +176,17 @@ public Authentication authenticate(Authentication authentication) throws Authent
176176
String issuer = this.issuerConverter.convert(token);
177177
AuthenticationManager authenticationManager = this.issuerAuthenticationManagerResolver.resolve(issuer);
178178
if (authenticationManager == null) {
179-
throw new InvalidBearerTokenException("Invalid issuer");
179+
AuthenticationException ex = new InvalidBearerTokenException("Invalid issuer");
180+
ex.setAuthenticationRequest(authentication);
181+
throw ex;
182+
}
183+
try {
184+
return authenticationManager.authenticate(authentication);
185+
}
186+
catch (AuthenticationException ex) {
187+
ex.setAuthenticationRequest(authentication);
188+
throw ex;
180189
}
181-
return authenticationManager.authenticate(authentication);
182190
}
183191

184192
}
@@ -194,10 +202,14 @@ public String convert(@NonNull BearerTokenAuthenticationToken authentication) {
194202
return issuer;
195203
}
196204
}
197-
catch (Exception ex) {
198-
throw new InvalidBearerTokenException(ex.getMessage(), ex);
205+
catch (Exception cause) {
206+
AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause);
207+
ex.setAuthenticationRequest(authentication);
208+
throw ex;
199209
}
200-
throw new InvalidBearerTokenException("Missing issuer");
210+
AuthenticationException ex = new InvalidBearerTokenException("Missing issuer");
211+
ex.setAuthenticationRequest(authentication);
212+
throw ex;
201213
}
202214

203215
}

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3737
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
3838
import org.springframework.security.core.Authentication;
39+
import org.springframework.security.core.AuthenticationException;
3940
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
4041
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
4142
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
@@ -181,8 +182,13 @@ public Mono<Authentication> authenticate(Authentication authentication) {
181182
BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
182183
return this.issuerConverter.convert(token)
183184
.flatMap((issuer) -> this.issuerAuthenticationManagerResolver.resolve(issuer)
184-
.switchIfEmpty(Mono.error(() -> new InvalidBearerTokenException("Invalid issuer " + issuer))))
185-
.flatMap((manager) -> manager.authenticate(authentication));
185+
.switchIfEmpty(Mono.error(() -> {
186+
AuthenticationException ex = new InvalidBearerTokenException("Invalid issuer " + issuer);
187+
ex.setAuthenticationRequest(authentication);
188+
return ex;
189+
})))
190+
.flatMap((manager) -> manager.authenticate(authentication))
191+
.doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication));
186192
}
187193

188194
}
@@ -194,12 +200,18 @@ public Mono<String> convert(@NonNull BearerTokenAuthenticationToken token) {
194200
try {
195201
String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer();
196202
if (issuer == null) {
197-
throw new InvalidBearerTokenException("Missing issuer");
203+
AuthenticationException ex = new InvalidBearerTokenException("Missing issuer");
204+
ex.setAuthenticationRequest(token);
205+
throw ex;
198206
}
199207
return Mono.just(issuer);
200208
}
201-
catch (Exception ex) {
202-
return Mono.error(() -> new InvalidBearerTokenException(ex.getMessage(), ex));
209+
catch (Exception cause) {
210+
return Mono.error(() -> {
211+
AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause);
212+
ex.setAuthenticationRequest(token);
213+
return ex;
214+
});
203215
}
204216
}
205217

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,14 +37,18 @@
3737
import org.springframework.security.authentication.AuthenticationManager;
3838
import org.springframework.security.authentication.AuthenticationManagerResolver;
3939
import org.springframework.security.core.Authentication;
40+
import org.springframework.security.core.AuthenticationException;
4041
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
4142
import org.springframework.security.oauth2.jose.TestKeys;
4243
import org.springframework.security.oauth2.jwt.JwtClaimNames;
44+
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
4345
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver;
4446

4547
import static org.assertj.core.api.Assertions.assertThat;
4648
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4749
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
50+
import static org.mockito.ArgumentMatchers.any;
51+
import static org.mockito.BDDMockito.given;
4852
import static org.mockito.BDDMockito.mock;
4953
import static org.mockito.BDDMockito.verify;
5054

@@ -263,6 +267,19 @@ public void resolveWhenBearerTokenEvilThenGenericException() {
263267
// @formatter:on
264268
}
265269

270+
@Test
271+
public void resolveWhenAuthenticationExceptionThenAuthenticationRequestIsIncluded() {
272+
Authentication authentication = new BearerTokenAuthenticationToken(this.jwt);
273+
AuthenticationException ex = new InvalidBearerTokenException("");
274+
AuthenticationManager manager = mock(AuthenticationManager.class);
275+
given(manager.authenticate(any())).willThrow(ex);
276+
JwtIssuerAuthenticationManagerResolver resolver = new JwtIssuerAuthenticationManagerResolver(
277+
(issuer) -> manager);
278+
assertThatExceptionOfType(InvalidBearerTokenException.class)
279+
.isThrownBy(() -> resolver.resolve(null).authenticate(authentication));
280+
assertThat(ex.getAuthenticationRequest()).isEqualTo(authentication);
281+
}
282+
266283
@Test
267284
public void factoryWhenNullOrEmptyIssuersThenException() {
268285
assertThatIllegalArgumentException()

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,13 +34,16 @@
3434
import okhttp3.mockwebserver.MockWebServer;
3535
import org.junit.jupiter.api.Test;
3636
import reactor.core.publisher.Mono;
37+
import reactor.test.StepVerifier;
3738

3839
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3940
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
4041
import org.springframework.security.core.Authentication;
42+
import org.springframework.security.core.AuthenticationException;
4143
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
4244
import org.springframework.security.oauth2.jose.TestKeys;
4345
import org.springframework.security.oauth2.jwt.JwtClaimNames;
46+
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
4447
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver;
4548

4649
import static org.assertj.core.api.Assertions.assertThat;
@@ -262,6 +265,20 @@ public void resolveWhenBearerTokenEvilThenGenericException() {
262265
// @formatter:on
263266
}
264267

268+
@Test
269+
public void resolveWhenAuthenticationExceptionThenAuthenticationRequestIsIncluded() {
270+
Authentication authentication = new BearerTokenAuthenticationToken(this.jwt);
271+
AuthenticationException ex = new InvalidBearerTokenException("");
272+
ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class);
273+
given(manager.authenticate(any())).willReturn(Mono.error(ex));
274+
JwtIssuerReactiveAuthenticationManagerResolver resolver = new JwtIssuerReactiveAuthenticationManagerResolver(
275+
(issuer) -> Mono.just(manager));
276+
StepVerifier.create(resolver.resolve(null).block().authenticate(authentication))
277+
.expectError(InvalidBearerTokenException.class)
278+
.verify();
279+
assertThat(ex.getAuthenticationRequest()).isEqualTo(authentication);
280+
}
281+
265282
@Test
266283
public void factoryWhenNullOrEmptyIssuersThenException() {
267284
assertThatIllegalArgumentException().isThrownBy(

web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2004-2022 the original author or authors.
2+
* Copyright 2004-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -194,10 +194,11 @@ private void handleAccessDeniedException(HttpServletRequest request, HttpServlet
194194
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
195195
authentication), exception);
196196
}
197-
sendStartAuthentication(request, response, chain,
198-
new InsufficientAuthenticationException(
199-
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
200-
"Full authentication is required to access this resource")));
197+
AuthenticationException ex = new InsufficientAuthenticationException(
198+
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
199+
"Full authentication is required to access this resource"));
200+
ex.setAuthenticationRequest(authentication);
201+
sendStartAuthentication(request, response, chain, ex);
201202
}
202203
else {
203204
if (logger.isTraceEnabled()) {

web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import org.springframework.security.authentication.AuthenticationManagerResolver;
2828
import org.springframework.security.authentication.AuthenticationServiceException;
2929
import org.springframework.security.authorization.AuthorizationManager;
30+
import org.springframework.security.core.AuthenticationException;
3031
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
3132
import org.springframework.security.web.util.matcher.RequestMatcher;
3233
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
@@ -46,7 +47,9 @@ public final class RequestMatcherDelegatingAuthenticationManagerResolver
4647
private final List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers;
4748

4849
private AuthenticationManager defaultAuthenticationManager = (authentication) -> {
49-
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
50+
AuthenticationException ex = new AuthenticationServiceException("Cannot authenticate " + authentication);
51+
ex.setAuthenticationRequest(authentication);
52+
throw ex;
5053
};
5154

5255
/**

web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.springframework.security.authentication.AuthenticationServiceException;
2727
import org.springframework.security.authentication.ReactiveAuthenticationManager;
2828
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
29+
import org.springframework.security.core.AuthenticationException;
2930
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
3031
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
3132
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
@@ -46,8 +47,11 @@ public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResol
4647

4748
private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> authenticationManagers;
4849

49-
private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono
50-
.error(new AuthenticationServiceException("Cannot authenticate " + authentication));
50+
private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> {
51+
AuthenticationException ex = new AuthenticationServiceException("Cannot authenticate " + authentication);
52+
ex.setAuthenticationRequest(authentication);
53+
return Mono.error(ex);
54+
};
5155

5256
/**
5357
* Construct an

web/src/main/java/org/springframework/security/web/server/authorization/ExceptionTranslationWebFilter.java

+3
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ private <T> Mono<T> commenceAuthentication(ServerWebExchange exchange, Authentic
101101
AuthenticationException cause = new InsufficientAuthenticationException(
102102
"Full authentication is required to access this resource");
103103
AuthenticationException ex = new AuthenticationCredentialsNotFoundException("Not Authenticated", cause);
104+
if (authentication != null) {
105+
ex.setAuthenticationRequest(authentication);
106+
}
104107
return this.authenticationEntryPoint.commence(exchange, ex).then(Mono.empty());
105108
}
106109

0 commit comments

Comments
 (0)