Skip to content

Commit 80f048d

Browse files
committed
Make Public Missing Authority AccessDeniedHandler
1 parent 298a156 commit 80f048d

File tree

10 files changed

+244
-273
lines changed

10 files changed

+244
-273
lines changed

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

Lines changed: 16 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,26 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19-
import java.io.IOException;
20-
import java.util.Collection;
2119
import java.util.LinkedHashMap;
22-
import java.util.List;
2320
import java.util.Map;
21+
import java.util.function.Consumer;
2422

25-
import jakarta.servlet.ServletException;
26-
import jakarta.servlet.http.HttpServletRequest;
27-
import jakarta.servlet.http.HttpServletResponse;
2823
import org.jspecify.annotations.Nullable;
2924

30-
import org.springframework.security.access.AccessDeniedException;
31-
import org.springframework.security.authentication.InsufficientAuthenticationException;
32-
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
33-
import org.springframework.security.authorization.AuthorizationDeniedException;
3425
import org.springframework.security.config.Customizer;
3526
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3627
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
37-
import org.springframework.security.core.AuthenticationException;
38-
import org.springframework.security.core.GrantedAuthority;
3928
import org.springframework.security.web.AuthenticationEntryPoint;
4029
import org.springframework.security.web.access.AccessDeniedHandler;
4130
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
31+
import org.springframework.security.web.access.DelegatingMissingAuthorityAccessDeniedHandler;
4232
import org.springframework.security.web.access.ExceptionTranslationFilter;
4333
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
4434
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
4535
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
4636
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
47-
import org.springframework.security.web.savedrequest.NullRequestCache;
4837
import org.springframework.security.web.savedrequest.RequestCache;
49-
import org.springframework.security.web.util.ThrowableAnalyzer;
50-
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
5138
import org.springframework.security.web.util.matcher.RequestMatcher;
52-
import org.springframework.util.Assert;
5339

5440
/**
5541
* Adds exception handling for Spring Security related exceptions to an application. All
@@ -96,6 +82,9 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
9682

9783
private Map<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entryPoints = new LinkedHashMap<>();
9884

85+
private DelegatingMissingAuthorityAccessDeniedHandler.Builder missingAuthoritiesHandlerBuilder =
86+
DelegatingMissingAuthorityAccessDeniedHandler.builder();
87+
9988
/**
10089
* Creates a new instance
10190
* @see HttpSecurity#exceptionHandling(Customizer)
@@ -117,6 +106,11 @@ public ExceptionHandlingConfigurer<H> accessDeniedPage(String accessDeniedUrl) {
117106
return accessDeniedHandler(accessDeniedHandler);
118107
}
119108

109+
public ExceptionHandlingConfigurer<H> missingAuthoritiesHandler(Consumer<DelegatingMissingAuthorityAccessDeniedHandler.Builder> consumer) {
110+
consumer.accept(this.missingAuthoritiesHandlerBuilder);
111+
return this;
112+
}
113+
120114
/**
121115
* Specifies the {@link AccessDeniedHandler} to be used
122116
* @param accessDeniedHandler the {@link AccessDeniedHandler} to be used
@@ -189,26 +183,6 @@ public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(Authent
189183
return this;
190184
}
191185

192-
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
193-
RequestMatcher preferredMatcher, String authority) {
194-
this.defaultEntryPointMappings.put(preferredMatcher, entryPoint);
195-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = this.entryPoints.get(authority);
196-
if (byMatcher == null) {
197-
byMatcher = new LinkedHashMap<>();
198-
}
199-
byMatcher.put(preferredMatcher, entryPoint);
200-
this.entryPoints.put(authority, byMatcher);
201-
return this;
202-
}
203-
204-
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
205-
String authority) {
206-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> byMatcher = new LinkedHashMap<>();
207-
byMatcher.put(AnyRequestMatcher.INSTANCE, entryPoint);
208-
this.entryPoints.put(authority, byMatcher);
209-
return this;
210-
}
211-
212186
/**
213187
* Gets any explicitly configured {@link AuthenticationEntryPoint}
214188
* @return
@@ -272,19 +246,10 @@ private AccessDeniedHandler createDefaultDeniedHandler(H http) {
272246
if (this.entryPoints.isEmpty()) {
273247
return defaults;
274248
}
275-
Map<String, AccessDeniedHandler> deniedHandlers = new LinkedHashMap<>();
276-
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
277-
.entrySet()) {
278-
AuthenticationEntryPoint entryPoint = entryPointFrom(entry.getValue());
279-
AuthenticationEntryPointAccessDeniedHandlerAdapter deniedHandler = new AuthenticationEntryPointAccessDeniedHandlerAdapter(
280-
entryPoint);
281-
RequestCache requestCache = http.getSharedObject(RequestCache.class);
282-
if (requestCache != null) {
283-
deniedHandler.setRequestCache(requestCache);
284-
}
285-
deniedHandlers.put(entry.getKey(), deniedHandler);
286-
}
287-
return new AuthenticationFactorDelegatingAccessDeniedHandler(deniedHandlers, defaults);
249+
DelegatingMissingAuthorityAccessDeniedHandler deniedHandler = this.missingAuthoritiesHandlerBuilder.build();
250+
deniedHandler.setRequestCache(getRequestCache(http));
251+
deniedHandler.setDefaultAccessDeniedHandler(defaults);
252+
return deniedHandler;
288253
}
289254

290255
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
@@ -299,29 +264,10 @@ private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
299264
}
300265

301266
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
302-
AuthenticationEntryPoint defaults = entryPointFrom(this.defaultEntryPointMappings);
303-
if (this.entryPoints.isEmpty()) {
304-
return defaults;
305-
}
306-
Map<String, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
307-
for (Map.Entry<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entry : this.entryPoints
308-
.entrySet()) {
309-
entryPoints.put(entry.getKey(), entryPointFrom(entry.getValue()));
310-
}
311-
return new AuthenticationFactorDelegatingAuthenticationEntryPoint(entryPoints, defaults);
312-
}
313-
314-
private AuthenticationEntryPoint entryPointFrom(
315-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints) {
316-
if (entryPoints.isEmpty()) {
267+
if (this.defaultEntryPoint == null) {
317268
return new Http403ForbiddenEntryPoint();
318269
}
319-
if (entryPoints.size() == 1) {
320-
return entryPoints.values().iterator().next();
321-
}
322-
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
323-
entryPoint.setDefaultEntryPoint(entryPoints.values().iterator().next());
324-
return entryPoint;
270+
return this.defaultEntryPoint.build();
325271
}
326272

327273
/**
@@ -340,128 +286,4 @@ private RequestCache getRequestCache(H http) {
340286
return new HttpSessionRequestCache();
341287
}
342288

343-
private static final class AuthenticationFactorDelegatingAuthenticationEntryPoint
344-
implements AuthenticationEntryPoint {
345-
346-
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
347-
348-
private final Map<String, AuthenticationEntryPoint> entryPoints;
349-
350-
private final AuthenticationEntryPoint defaults;
351-
352-
private AuthenticationFactorDelegatingAuthenticationEntryPoint(
353-
Map<String, AuthenticationEntryPoint> entryPoints, AuthenticationEntryPoint defaults) {
354-
this.entryPoints = new LinkedHashMap<>(entryPoints);
355-
this.defaults = defaults;
356-
}
357-
358-
@Override
359-
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
360-
throws IOException, ServletException {
361-
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
362-
entryPoint(authorization).commence(request, response, ex);
363-
}
364-
365-
private AuthenticationEntryPoint entryPoint(Collection<GrantedAuthority> authorities) {
366-
if (authorities == null) {
367-
return this.defaults;
368-
}
369-
for (GrantedAuthority needed : authorities) {
370-
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
371-
if (entryPoint != null) {
372-
return entryPoint;
373-
}
374-
}
375-
return this.defaults;
376-
}
377-
378-
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
379-
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
380-
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
381-
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
382-
if (denied == null) {
383-
return List.of();
384-
}
385-
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
386-
return List.of();
387-
}
388-
return authorization.getAuthorities();
389-
}
390-
391-
}
392-
393-
private static final class AuthenticationEntryPointAccessDeniedHandlerAdapter implements AccessDeniedHandler {
394-
395-
private final AuthenticationEntryPoint entryPoint;
396-
397-
private RequestCache requestCache = new NullRequestCache();
398-
399-
private AuthenticationEntryPointAccessDeniedHandlerAdapter(AuthenticationEntryPoint entryPoint) {
400-
this.entryPoint = entryPoint;
401-
}
402-
403-
void setRequestCache(RequestCache requestCache) {
404-
Assert.notNull(requestCache, "requestCache cannot be null");
405-
this.requestCache = requestCache;
406-
}
407-
408-
@Override
409-
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException denied)
410-
throws IOException, ServletException {
411-
AuthenticationException ex = new InsufficientAuthenticationException("access denied", denied);
412-
this.requestCache.saveRequest(request, response);
413-
this.entryPoint.commence(request, response, ex);
414-
}
415-
416-
}
417-
418-
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
419-
420-
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
421-
422-
private final Map<String, AccessDeniedHandler> deniedHandlers;
423-
424-
private final AccessDeniedHandler defaults;
425-
426-
private AuthenticationFactorDelegatingAccessDeniedHandler(Map<String, AccessDeniedHandler> deniedHandlers,
427-
AccessDeniedHandler defaults) {
428-
this.deniedHandlers = new LinkedHashMap<>(deniedHandlers);
429-
this.defaults = defaults;
430-
}
431-
432-
@Override
433-
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
434-
throws IOException, ServletException {
435-
Collection<GrantedAuthority> authorization = authorizationRequest(ex);
436-
deniedHandler(authorization).handle(request, response, ex);
437-
}
438-
439-
private AccessDeniedHandler deniedHandler(Collection<GrantedAuthority> authorities) {
440-
if (authorities == null) {
441-
return this.defaults;
442-
}
443-
for (GrantedAuthority needed : authorities) {
444-
AccessDeniedHandler deniedHandler = this.deniedHandlers.get(needed.getAuthority());
445-
if (deniedHandler != null) {
446-
return deniedHandler;
447-
}
448-
}
449-
return this.defaults;
450-
}
451-
452-
private Collection<GrantedAuthority> authorizationRequest(Exception ex) {
453-
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
454-
AuthorizationDeniedException denied = (AuthorizationDeniedException) this.throwableAnalyzer
455-
.getFirstThrowableOfType(AuthorizationDeniedException.class, chain);
456-
if (denied == null) {
457-
return List.of();
458-
}
459-
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
460-
return List.of();
461-
}
462-
return authorization.getAuthorities();
463-
}
464-
465-
}
466-
467289
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ public void init(H http) throws Exception {
233233
initDefaultLoginFilter(http);
234234
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
235235
if (exceptions != null) {
236-
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_PASSWORD");
236+
exceptions.missingAuthoritiesHandler((commence) -> commence
237+
.authorities("FACTOR_PASSWORD")
238+
.whenRequestMatches(getAuthenticationEntryPointMatcher(http))
239+
.commence(getAuthenticationEntryPoint())
240+
);
237241
}
238242
}
239243

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,13 @@ private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher)
192192
if (exceptionHandling == null) {
193193
return;
194194
}
195-
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(this.authenticationEntryPoint),
196-
preferredMatcher);
195+
AuthenticationEntryPoint entryPoint = postProcess(this.authenticationEntryPoint);
196+
exceptionHandling.defaultAuthenticationEntryPointFor(entryPoint, preferredMatcher);
197+
exceptionHandling.missingAuthoritiesHandler((handler) -> handler
198+
.authorities("FACTOR_PASSWORD")
199+
.whenRequestMatches(preferredMatcher)
200+
.commence(entryPoint)
201+
);
197202
}
198203

199204
private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ public WebAuthnConfigurer<H> creationOptionsRepository(
155155
public void init(H http) throws Exception {
156156
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
157157
if (exceptions != null) {
158-
exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),
159-
"FACTOR_WEBAUTHN");
158+
exceptions.missingAuthoritiesHandler((handler) -> handler
159+
.authorities("FACTOR_WEBAUTHN")
160+
.commence(new LoginUrlAuthenticationEntryPoint("/login"))
161+
);
160162
}
161163
}
162164

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,16 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19-
import java.util.List;
20-
2119
import jakarta.servlet.http.HttpServletRequest;
22-
import org.jspecify.annotations.Nullable;
2320

2421
import org.springframework.context.ApplicationContext;
2522
import org.springframework.security.authentication.AuthenticationDetailsSource;
2623
import org.springframework.security.authentication.AuthenticationManager;
27-
import org.springframework.security.authentication.AuthenticationProvider;
2824
import org.springframework.security.config.Customizer;
2925
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3026
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3127
import org.springframework.security.core.Authentication;
32-
import org.springframework.security.core.AuthenticationException;
3328
import org.springframework.security.core.authority.AuthorityUtils;
34-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3529
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
3630
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
3731
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -44,7 +38,6 @@
4438
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
4539
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
4640
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
47-
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
4841

4942
/**
5043
* Adds X509 based pre authentication to an application. Since validating the certificate
@@ -190,8 +183,10 @@ public void init(H http) {
190183
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
191184
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
192185
if (exceptions != null) {
193-
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE,
194-
"FACTOR_X509");
186+
exceptions.missingAuthoritiesHandler((handler) -> handler
187+
.authorities("FACTOR_X509")
188+
.commence(new Http403ForbiddenEntryPoint())
189+
);
195190
}
196191
}
197192

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,10 +373,6 @@ public void init(B http) throws Exception {
373373
http.authenticationProvider(new OidcAuthenticationRequestChecker());
374374
}
375375
this.initDefaultLoginFilter(http);
376-
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
377-
if (exceptions != null) {
378-
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_AUTHORIZATION_CODE");
379-
}
380376
}
381377

382378
@Override
@@ -561,11 +557,20 @@ private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLogin
561557
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
562558
new NegatedRequestMatcher(defaultLoginPageMatcher), formLoginNotEnabled);
563559
// @formatter:off
564-
return DelegatingAuthenticationEntryPoint.builder()
560+
AuthenticationEntryPoint loginEntryPoint = DelegatingAuthenticationEntryPoint.builder()
565561
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
566562
.defaultEntryPoint(getAuthenticationEntryPoint())
567563
.build();
568564
// @formatter:on
565+
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
566+
if (exceptions != null) {
567+
exceptions.missingAuthoritiesHandler((handler) -> handler
568+
.authorities("FACTOR_AUTHORIZATION_CODE")
569+
.whenRequestMatches(getAuthenticationEntryPointMatcher(http))
570+
.commence(loginEntryPoint)
571+
);
572+
}
573+
return loginEntryPoint;
569574
}
570575

571576
private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {

0 commit comments

Comments
 (0)