Skip to content

Commit e66c498

Browse files
committed
Redirect to Appropriate Entry Point Based on Missing Authorities
Issue gh-17934
1 parent fe17f29 commit e66c498

File tree

9 files changed

+258
-76
lines changed

9 files changed

+258
-76
lines changed

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

Lines changed: 159 additions & 74 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;
@@ -35,29 +34,22 @@
3534
import org.springframework.security.config.Customizer;
3635
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3736
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
38-
import org.springframework.security.core.Authentication;
3937
import org.springframework.security.core.AuthenticationException;
4038
import org.springframework.security.core.GrantedAuthority;
41-
import org.springframework.security.core.context.SecurityContextHolder;
42-
import org.springframework.security.core.context.SecurityContextHolderStrategy;
43-
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
4439
import org.springframework.security.web.AuthenticationEntryPoint;
45-
import org.springframework.security.web.FormPostRedirectStrategy;
46-
import org.springframework.security.web.RedirectStrategy;
4740
import org.springframework.security.web.access.AccessDeniedHandler;
4841
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
4942
import org.springframework.security.web.access.ExceptionTranslationFilter;
5043
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
5144
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
5245
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
53-
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
54-
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
55-
import org.springframework.security.web.csrf.CsrfToken;
5646
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
47+
import org.springframework.security.web.savedrequest.NullRequestCache;
5748
import org.springframework.security.web.savedrequest.RequestCache;
49+
import org.springframework.security.web.util.ThrowableAnalyzer;
50+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
5851
import org.springframework.security.web.util.matcher.RequestMatcher;
5952
import org.springframework.util.Assert;
60-
import org.springframework.web.util.UriComponentsBuilder;
6153

6254
/**
6355
* Adds exception handling for Spring Security related exceptions to an application. All
@@ -102,6 +94,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
10294

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

97+
private Map<String, LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>> entryPoints = new LinkedHashMap<>();
98+
10599
/**
106100
* Creates a new instance
107101
* @see HttpSecurity#exceptionHandling(Customizer)
@@ -195,6 +189,26 @@ public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(Authent
195189
return this;
196190
}
197191

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+
198212
/**
199213
* Gets any explicitly configured {@link AuthenticationEntryPoint}
200214
* @return
@@ -254,21 +268,60 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
254268
}
255269

256270
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
271+
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);
288+
}
289+
290+
private AccessDeniedHandler createDefaultAccessDeniedHandler(H http) {
257291
if (this.defaultDeniedHandlerMappings.isEmpty()) {
258-
return new AuthenticationFactorDelegatingAccessDeniedHandler();
292+
return new AccessDeniedHandlerImpl();
259293
}
260294
if (this.defaultDeniedHandlerMappings.size() == 1) {
261295
return this.defaultDeniedHandlerMappings.values().iterator().next();
262296
}
263297
return new RequestMatcherDelegatingAccessDeniedHandler(this.defaultDeniedHandlerMappings,
264-
new AuthenticationFactorDelegatingAccessDeniedHandler());
298+
new AccessDeniedHandlerImpl());
265299
}
266300

267301
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
268-
if (this.defaultEntryPoint == null) {
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()) {
269317
return new Http403ForbiddenEntryPoint();
270318
}
271-
return this.defaultEntryPoint.build();
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;
272325
}
273326

274327
/**
@@ -287,94 +340,126 @@ private RequestCache getRequestCache(H http) {
287340
return new HttpSessionRequestCache();
288341
}
289342

290-
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
343+
private static final class AuthenticationFactorDelegatingAuthenticationEntryPoint
344+
implements AuthenticationEntryPoint {
345+
346+
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
291347

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

301-
private final AccessDeniedHandler defaults = new AccessDeniedHandlerImpl();
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+
}
302357

303358
@Override
304-
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
359+
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
305360
throws IOException, ServletException {
306-
Collection<String> needed = authorizationRequest(ex);
307-
if (needed == null) {
308-
this.defaults.handle(request, response, ex);
309-
return;
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;
310368
}
311-
for (String authority : needed) {
312-
AuthenticationEntryPoint entryPoint = this.entryPoints.get(authority);
369+
for (GrantedAuthority needed : authorities) {
370+
AuthenticationEntryPoint entryPoint = this.entryPoints.get(needed.getAuthority());
313371
if (entryPoint != null) {
314-
AuthenticationException insufficient = new InsufficientAuthenticationException(ex.getMessage(), ex);
315-
entryPoint.commence(request, response, insufficient);
316-
return;
372+
return entryPoint;
317373
}
318374
}
319-
this.defaults.handle(request, response, ex);
375+
return this.defaults;
320376
}
321377

322-
private Collection<String> authorizationRequest(AccessDeniedException access) {
323-
if (!(access instanceof AuthorizationDeniedException denied)) {
324-
return null;
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();
325384
}
326-
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision decision)) {
327-
return null;
385+
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
386+
return List.of();
328387
}
329-
return decision.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
388+
return authorization.getAuthorities();
330389
}
331390

332391
}
333392

334-
private static final class PostAuthenticationEntryPoint implements AuthenticationEntryPoint {
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+
}
335415

336-
private final String entryPointUri;
416+
}
417+
418+
private static final class AuthenticationFactorDelegatingAccessDeniedHandler implements AccessDeniedHandler {
337419

338-
private final Map<String, Function<Authentication, String>> params;
420+
private final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
339421

340-
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
341-
.getContextHolderStrategy();
422+
private final Map<String, AccessDeniedHandler> deniedHandlers;
342423

343-
private RedirectStrategy redirectStrategy = new FormPostRedirectStrategy();
424+
private final AccessDeniedHandler defaults;
344425

345-
private PostAuthenticationEntryPoint(String entryPointUri,
346-
Map<String, Function<Authentication, String>> params) {
347-
this.entryPointUri = entryPointUri;
348-
this.params = params;
426+
private AuthenticationFactorDelegatingAccessDeniedHandler(Map<String, AccessDeniedHandler> deniedHandlers,
427+
AccessDeniedHandler defaults) {
428+
this.deniedHandlers = new LinkedHashMap<>(deniedHandlers);
429+
this.defaults = defaults;
349430
}
350431

351432
@Override
352-
public void commence(HttpServletRequest request, HttpServletResponse response,
353-
AuthenticationException authException) throws IOException, ServletException {
354-
Authentication authentication = getAuthentication(authException);
355-
Assert.notNull(authentication, "could not find authentication in order to perform post");
356-
Map<String, String> params = this.params.entrySet()
357-
.stream()
358-
.collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().apply(authentication)));
359-
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(this.entryPointUri);
360-
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
361-
if (csrf != null) {
362-
builder.queryParam(csrf.getParameterName(), csrf.getToken());
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+
}
363448
}
364-
String entryPointUrl = builder.build(false).expand(params).toUriString();
365-
this.redirectStrategy.sendRedirect(request, response, entryPointUrl);
449+
return this.defaults;
366450
}
367451

368-
private Authentication getAuthentication(AuthenticationException authException) {
369-
Authentication authentication = authException.getAuthenticationRequest();
370-
if (authentication != null && authentication.isAuthenticated()) {
371-
return authentication;
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();
372458
}
373-
authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
374-
if (authentication != null && authentication.isAuthenticated()) {
375-
return authentication;
459+
if (!(denied.getAuthorizationResult() instanceof AuthorityAuthorizationDecision authorization)) {
460+
return List.of();
376461
}
377-
return null;
462+
return authorization.getAuthorities();
378463
}
379464

380465
}

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ 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);
187+
exceptions.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), AnyRequestMatcher.INSTANCE,
188+
"FACTOR_X509");
188189
}
189190
}
190191

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4141
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
4242
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
43+
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
4344
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
4445
import org.springframework.security.context.DelegatingApplicationListener;
4546
import org.springframework.security.core.Authentication;
@@ -372,6 +373,10 @@ public void init(B http) throws Exception {
372373
http.authenticationProvider(new OidcAuthenticationRequestChecker());
373374
}
374375
this.initDefaultLoginFilter(http);
376+
ExceptionHandlingConfigurer<B> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
377+
if (exceptions != null) {
378+
exceptions.defaultAuthenticationEntryPointFor(getAuthenticationEntryPoint(), "FACTOR_AUTHORIZATION_CODE");
379+
}
375380
}
376381

377382
@Override

0 commit comments

Comments
 (0)