Skip to content

Commit 9f31775

Browse files
committed
Make Public Missing Authority AccessDeniedHandler
Issue spring-projectsgh-17934
1 parent df7a7cd commit 9f31775

File tree

12 files changed

+451
-287
lines changed

12 files changed

+451
-287
lines changed

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

Lines changed: 41 additions & 198 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;
23-
import java.util.Map;
20+
import java.util.function.Consumer;
2421

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

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;
3424
import org.springframework.security.config.Customizer;
3525
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3626
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
37-
import org.springframework.security.core.AuthenticationException;
3827
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
@@ -94,7 +80,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
9480

9581
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
9682

97-
private Map<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entryPoints = new LinkedHashMap<>();
83+
private final DelegatingMissingAuthorityAccessDeniedHandler.Builder missingAuthoritiesHandlerBuilder = DelegatingMissingAuthorityAccessDeniedHandler
84+
.builder();
9885

9986
/**
10087
* Creates a new instance
@@ -146,6 +133,37 @@ public ExceptionHandlingConfigurer<H> defaultAccessDeniedHandlerFor(AccessDenied
146133
return this;
147134
}
148135

136+
/**
137+
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
138+
* invoked for the provided missing {@link GrantedAuthority}.
139+
* @param entryPoint the {@link AuthenticationEntryPoint} to use for the given
140+
* {@code authority}
141+
* @param authority the authority
142+
* @return the {@link ExceptionHandlingConfigurer} for further customizations
143+
* @since 7.0
144+
*/
145+
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
146+
String authority) {
147+
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
148+
return this;
149+
}
150+
151+
/**
152+
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
153+
* invoked for the provided missing {@link GrantedAuthority}.
154+
* @param entryPoint a consumer of a
155+
* {@link DelegatingAuthenticationEntryPoint.Builder} to use for the given
156+
* {@code authority}
157+
* @param authority the authority
158+
* @return the {@link ExceptionHandlingConfigurer} for further customizations
159+
* @since 7.0
160+
*/
161+
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(
162+
Consumer<DelegatingAuthenticationEntryPoint.Builder> entryPoint, String authority) {
163+
this.missingAuthoritiesHandlerBuilder.addEntryPointFor(entryPoint, authority);
164+
return this;
165+
}
166+
149167
/**
150168
* Sets the {@link AuthenticationEntryPoint} to be used.
151169
*
@@ -189,26 +207,6 @@ public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(Authent
189207
return this;
190208
}
191209

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-
212210
/**
213211
* Gets any explicitly configured {@link AuthenticationEntryPoint}
214212
* @return
@@ -269,22 +267,10 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
269267

270268
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
271269
AccessDeniedHandler defaults = createDefaultAccessDeniedHandler(http);
272-
if (this.entryPoints.isEmpty()) {
273-
return defaults;
274-
}
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);
270+
DelegatingMissingAuthorityAccessDeniedHandler deniedHandler = this.missingAuthoritiesHandlerBuilder.build();
271+
deniedHandler.setRequestCache(getRequestCache(http));
272+
deniedHandler.setDefaultAccessDeniedHandler(defaults);
273+
return deniedHandler;
288274
}
289275

290276
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
@@ -299,29 +285,10 @@ private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
299285
}
300286

301287
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()) {
288+
if (this.defaultEntryPoint == null) {
317289
return new Http403ForbiddenEntryPoint();
318290
}
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;
291+
return this.defaultEntryPoint.build();
325292
}
326293

327294
/**
@@ -340,128 +307,4 @@ private RequestCache getRequestCache(H http) {
340307
return new HttpSessionRequestCache();
341308
}
342309

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-
467310
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,10 @@ 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+
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint();
237+
RequestMatcher requestMatcher = getAuthenticationEntryPointMatcher(http);
238+
exceptions.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(entryPoint, requestMatcher),
239+
"FACTOR_PASSWORD");
237240
}
238241
}
239242

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ 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.defaultAuthenticationEntryPointFor((ep) -> ep.addEntryPointFor(entryPoint, preferredMatcher),
198+
"FACTOR_PASSWORD");
197199
}
198200

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
import org.springframework.security.authentication.ProviderManager;
2828
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2929
import org.springframework.security.core.userdetails.UserDetailsService;
30+
import org.springframework.security.web.AuthenticationEntryPoint;
3031
import org.springframework.security.web.access.intercept.AuthorizationFilter;
3132
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
3233
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
3334
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
3435
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
3536
import org.springframework.security.web.csrf.CsrfToken;
37+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
3638
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
3739
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
3840
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
@@ -155,8 +157,9 @@ public WebAuthnConfigurer<H> creationOptionsRepository(
155157
public void init(H http) throws Exception {
156158
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
157159
if (exceptions != null) {
158-
exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),
159-
"FACTOR_WEBAUTHN");
160+
AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/login");
161+
exceptions.defaultAuthenticationEntryPointFor(
162+
(ep) -> ep.addEntryPointFor(entryPoint, AnyRequestMatcher.INSTANCE), "FACTOR_WEBAUTHN");
160163
}
161164
}
162165

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,9 @@ public void init(H http) {
184184
.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
185185
ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
186186
if (exceptions != null) {
187-
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE,
188-
"FACTOR_X509");
187+
AuthenticationEntryPoint forbidden = new Http403ForbiddenEntryPoint();
188+
exceptions.defaultAuthenticationEntryPointFor(
189+
(ep) -> ep.addEntryPointFor(forbidden, AnyRequestMatcher.INSTANCE), "FACTOR_X509");
189190
}
190191
}
191192

0 commit comments

Comments
 (0)