Skip to content

Commit 710fafc

Browse files
committed
ExceptionHandlingConfigurer
1 parent ced9ad0 commit 710fafc

File tree

9 files changed

+257
-84
lines changed

9 files changed

+257
-84
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

Lines changed: 157 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@
1919
import java.io.IOException;
2020
import java.util.Collection;
2121
import java.util.LinkedHashMap;
22+
import java.util.List;
2223
import java.util.Map;
23-
import java.util.function.Function;
24-
import java.util.stream.Collectors;
2524

2625
import jakarta.servlet.ServletException;
2726
import jakarta.servlet.http.HttpServletRequest;
@@ -34,29 +33,22 @@
3433
import org.springframework.security.config.Customizer;
3534
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3635
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
37-
import org.springframework.security.core.Authentication;
3836
import org.springframework.security.core.AuthenticationException;
3937
import org.springframework.security.core.GrantedAuthority;
40-
import org.springframework.security.core.context.SecurityContextHolder;
41-
import org.springframework.security.core.context.SecurityContextHolderStrategy;
42-
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
4338
import org.springframework.security.web.AuthenticationEntryPoint;
44-
import org.springframework.security.web.FormPostRedirectStrategy;
45-
import org.springframework.security.web.RedirectStrategy;
4639
import org.springframework.security.web.access.AccessDeniedHandler;
4740
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
4841
import org.springframework.security.web.access.ExceptionTranslationFilter;
4942
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
5043
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
5144
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
52-
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
53-
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
54-
import org.springframework.security.web.csrf.CsrfToken;
5545
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
46+
import org.springframework.security.web.savedrequest.NullRequestCache;
5647
import org.springframework.security.web.savedrequest.RequestCache;
48+
import org.springframework.security.web.util.ThrowableAnalyzer;
49+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
5750
import org.springframework.security.web.util.matcher.RequestMatcher;
5851
import org.springframework.util.Assert;
59-
import org.springframework.web.util.UriComponentsBuilder;
6052

6153
/**
6254
* Adds exception handling for Spring Security related exceptions to an application. All
@@ -101,6 +93,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
10193

10294
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
10395

96+
private Map<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entryPoints = new LinkedHashMap<>();
97+
10498
/**
10599
* Creates a new instance
106100
* @see HttpSecurity#exceptionHandling(Customizer)
@@ -191,6 +185,26 @@ public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(Authent
191185
return this;
192186
}
193187

188+
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
189+
RequestMatcher preferredMatcher, String authority) {
190+
this.defaultEntryPointMappings.put(preferredMatcher, entryPoint);
191+
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = this.entryPoints.get(authority);
192+
if (byMatcher == null) {
193+
byMatcher = new LinkedHashMap<>();
194+
}
195+
byMatcher.put(preferredMatcher, entryPoint);
196+
this.entryPoints.put(authority, byMatcher);
197+
return this;
198+
}
199+
200+
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
201+
String authority) {
202+
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = new LinkedHashMap<>();
203+
byMatcher.put(AnyRequestMatcher.INSTANCE, entryPoint);
204+
this.entryPoints.put(authority, byMatcher);
205+
return this;
206+
}
207+
194208
/**
195209
* Gets any explicitly configured {@link AuthenticationEntryPoint}
196210
* @return
@@ -250,26 +264,59 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
250264
}
251265

252266
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
267+
AccessDeniedHandler defaults = createDefaultAccessDeniedHandler(http);
268+
if (this.entryPoints.isEmpty()) {
269+
return defaults;
270+
}
271+
Map<String, AccessDeniedHandler> deniedHandlers = new LinkedHashMap<>();
272+
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
273+
.entrySet()) {
274+
AuthenticationEntryPoint entryPoint = entryPointFrom(entry.getValue());
275+
AuthenticationEntryPointAccessDeniedHandlerAdapter deniedHandler = new AuthenticationEntryPointAccessDeniedHandlerAdapter(
276+
entryPoint);
277+
RequestCache requestCache = http.getSharedObject(RequestCache.class);
278+
if (requestCache != null) {
279+
deniedHandler.setRequestCache(requestCache);
280+
}
281+
deniedHandlers.put(entry.getKey(), deniedHandler);
282+
}
283+
return new AuthenticationFactorDelegatingAccessDeniedHandler(deniedHandlers, defaults);
284+
}
285+
286+
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
253287
if (this.defaultDeniedHandlerMappings.isEmpty()) {
254-
return new AuthenticationFactorDelegatingAccessDeniedHandler();
288+
return new AccessDeniedHandlerImpl();
255289
}
256290
if (this.defaultDeniedHandlerMappings.size() == 1) {
257291
return this.defaultDeniedHandlerMappings.values().iterator().next();
258292
}
259293
return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
260-
new AuthenticationFactorDelegatingAccessDeniedHandler());
294+
new AccessDeniedHandlerImpl());
261295
}
262296

263297
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
264-
if (this.defaultEntryPointMappings.isEmpty()) {
298+
AuthenticationEntryPoint defaults = entryPointFrom(this.defaultEntryPointMappings);
299+
if (this.entryPoints.isEmpty()) {
300+
return defaults;
301+
}
302+
Map<String, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
303+
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
304+
.entrySet()) {
305+
entryPoints.put(entry.getKey(), entryPointFrom(entry.getValue()));
306+
}
307+
return new AuthenticationFactorDelegatingAuthenticationEntryPoint(entryPoints, defaults);
308+
}
309+
310+
private AuthenticationEntryPoint entryPointFrom(
311+
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints) {
312+
if (entryPoints.isEmpty()) {
265313
return new Http403ForbiddenEntryPoint();
266314
}
267-
if (this.defaultEntryPointMappings.size() == 1) {
268-
return this.defaultEntryPointMappings.values().iterator().next();
315+
if (entryPoints.size() == 1) {
316+
return entryPoints.values().iterator().next();
269317
}
270-
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
271-
this.defaultEntryPointMappings);
272-
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
318+
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
319+
entryPoint.setDefaultEntryPoint(entryPoints.values().iterator().next());
273320
return entryPoint;
274321
}
275322

@@ -289,94 +336,126 @@ private RequestCache getRequestCache(H http) {
289336
return new HttpSessionRequestCache();
290337
}
291338

292-
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
339+
private static final class AuthenticationFactorDelegatingAuthenticationEntryPoint
340+
implements AuthenticationEntryPoint {
341+
342+
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
343+
344+
private final Map<String, AuthenticationEntryPoint> entryPoints;
293345

294-
private final Map<String, AuthenticationEntryPoint> entryPoints = Map.of("FACTOR_PASSWORD",
295-
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_AUTHORIZATION_CODE",
296-
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_SAML_RESPONSE",
297-
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_WEBAUTHN",
298-
new LoginUrlAuthenticationEntryPoint("/login"), "FACTOR_BEARER",
299-
new BearerTokenAuthenticationEntryPoint(), "FACTOR_OTT",
300-
new PostAuthenticationEntryPoint(GenerateOneTimeTokenFilter.DEFAULT_GENERATE_URL + "?username={u}",
301-
Map.of("u", Authentication::getName)));
346+
private final AuthenticationEntryPoint defaults;
302347

303-
private final AccessDeniedHandler defaults = new AccessDeniedHandlerImpl();
348+
private AuthenticationFactorDelegatingAuthenticationEntryPoint(
349+
Map<String, AuthenticationEntryPoint> entryPoints, AuthenticationEntryPoint defaults) {
350+
this.entryPoints = new LinkedHashMap<>(entryPoints);
351+
this.defaults = defaults;
352+
}
304353

305354
@Override
306-
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
355+
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
307356
throws IOException, ServletException {
308-
Collection<String> needed = authorizationRequest(ex);
309-
if (needed == null) {
310-
this.defaults.handle(request, response, ex);
311-
return;
357+
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
358+
entryPoint(authorization).commence(request, response, ex);
359+
}
360+
361+
private AuthenticationEntryPoint entryPoint(Collection<GrantedAuthority> authorities) {
362+
if (authorities == null) {
363+
return this.defaults;
312364
}
313-
for (String authority : needed) {
314-
AuthenticationEntryPoint entryPoint = this.entryPoints.get(authority);
365+
for (GrantedAuthority needed : authorities) {
366+
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
315367
if (entryPoint != null) {
316-
AuthenticationException insufficient = new InsufficientAuthenticationException(ex.getMessage(), ex);
317-
entryPoint.commence(request, response, insufficient);
318-
return;
368+
return entryPoint;
319369
}
320370
}
321-
this.defaults.handle(request, response, ex);
371+
return this.defaults;
322372
}
323373

324-
private Collection<String> authorizationRequest(AccessDeniedException access) {
325-
if (!(access instanceof AuthorizationDeniedException denied)) {
326-
return null;
374+
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
375+
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
376+
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
377+
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
378+
if (denied == null) {
379+
return List.of();
327380
}
328-
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision decision)) {
329-
return null;
381+
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
382+
return List.of();
330383
}
331-
return decision.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
384+
return authorization.getAuthorities();
332385
}
333386

334387
}
335388

336-
private static final class PostAuthenticationEntryPoint implements AuthenticationEntryPoint {
389+
private static final class AuthenticationEntryPointAccessDeniedHandlerAdapter implements AccessDeniedHandler {
337390

338-
private final String entryPointUri;
391+
private final AuthenticationEntryPoint entryPoint;
339392

340-
private final Map<String, Function<Authentication, String>> params;
393+
private RequestCache requestCache = new NullRequestCache();
341394

342-
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
343-
.getContextHolderStrategy();
395+
private AuthenticationEntryPointAccessDeniedHandlerAdapter(AuthenticationEntryPoint entryPoint) {
396+
this.entryPoint = entryPoint;
397+
}
344398

345-
private RedirectStrategy redirectStrategy = new FormPostRedirectStrategy();
399+
void setRequestCache(RequestCache requestCache) {
400+
Assert.notNull(requestCache, "requestCache cannot be null");
401+
this.requestCache = requestCache;
402+
}
346403

347-
private PostAuthenticationEntryPoint(String entryPointUri,
348-
Map<String, Function<Authentication, String>> params) {
349-
this.entryPointUri = entryPointUri;
350-
this.params = params;
404+
@Override
405+
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
406+
throws IOException, ServletException {
407+
AuthenticationException ex = new InsufficientAuthenticationException("access denied", denied);
408+
this.requestCache.saveRequest(request, response);
409+
this.entryPoint.commence(request, response, ex);
410+
}
411+
412+
}
413+
414+
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
415+
416+
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
417+
418+
private final Map<String, AccessDeniedHandler> deniedHandlers;
419+
420+
private final AccessDeniedHandler defaults;
421+
422+
private AuthenticationFactorDelegatingAccessDeniedHandler(Map<String, AccessDeniedHandler> deniedHandlers,
423+
AccessDeniedHandler defaults) {
424+
this.deniedHandlers = new LinkedHashMap<>(deniedHandlers);
425+
this.defaults = defaults;
351426
}
352427

353428
@Override
354-
public void commence(HttpServletRequest request, HttpServletResponse response,
355-
AuthenticationException authException) throws IOException, ServletException {
356-
Authentication authentication = getAuthentication(authException);
357-
Assert.notNull(authentication, "could not find authentication in order to perform post");
358-
Map<String, String> params = this.params.entrySet()
359-
.stream()
360-
.collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().apply(authentication)));
361-
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(this.entryPointUri);
362-
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
363-
if (csrf != null) {
364-
builder.queryParam(csrf.getParameterName(), csrf.getToken());
429+
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
430+
throws IOException, ServletException {
431+
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
432+
deniedHandler(authorization).handle(request, response, ex);
433+
}
434+
435+
private AccessDeniedHandler deniedHandler(Collection<GrantedAuthority> authorities) {
436+
if (authorities == null) {
437+
return this.defaults;
438+
}
439+
for (GrantedAuthority needed : authorities) {
440+
AccessDeniedHandler deniedHandler = this.deniedHandlers.get(needed.getAuthority());
441+
if (deniedHandler != null) {
442+
return deniedHandler;
443+
}
365444
}
366-
String entryPointUrl = builder.build(false).expand(params).toUriString();
367-
this.redirectStrategy.sendRedirect(request, response, entryPointUrl);
445+
return this.defaults;
368446
}
369447

370-
private Authentication getAuthentication(AuthenticationException authException) {
371-
Authentication authentication = authException.getAuthenticationRequest();
372-
if (authentication != null && authentication.isAuthenticated()) {
373-
return authentication;
448+
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
449+
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
450+
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
451+
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
452+
if (denied == null) {
453+
return List.of();
374454
}
375-
authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
376-
if (authentication != null && authentication.isAuthenticated()) {
377-
return authentication;
455+
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
456+
return List.of();
378457
}
379-
return null;
458+
return authorization.getAuthorities();
380459
}
381460

382461
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
231231
public void init(H http) throws Exception {
232232
super.init(http);
233233
initDefaultLoginFilter(http);
234+
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
235+
if (exceptions != null) {
236+
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_PASSWORD");
237+
}
234238
}
235239

236240
@Override

config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2929
import org.springframework.security.core.userdetails.UserDetailsService;
3030
import org.springframework.security.web.access.intercept.AuthorizationFilter;
31+
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
3132
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
3233
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
3334
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@@ -150,6 +151,15 @@ public WebAuthnConfigurer<H> creationOptionsRepository(
150151
return this;
151152
}
152153

154+
@Override
155+
public void init(H http) throws Exception {
156+
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
157+
if (exceptions != null) {
158+
exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),
159+
"FACTOR_WEBAUTHN");
160+
}
161+
}
162+
153163
@Override
154164
public void configure(H http) throws Exception {
155165
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class)

config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ public void init(H http) {
186186
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
187187
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
188188
if (exceptions != null) {
189-
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE);
189+
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE,
190+
"FACTOR_X509");
190191
}
191192
}
192193

@@ -248,10 +249,7 @@ private AuthorityGrantingAuthenticationProvider(AuthenticationProvider delegate)
248249
if (result == null) {
249250
return result;
250251
}
251-
return result
252-
.toBuilder()
253-
.authorities((a) -> a.add(new SimpleGrantedAuthority("FACTOR_X509")))
254-
.build();
252+
return result.toBuilder().authorities((a) -> a.add(new SimpleGrantedAuthority("FACTOR_X509"))).build();
255253
}
256254

257255
@Override

0 commit comments

Comments
 (0)