1919import java .io .IOException ;
2020import java .util .Collection ;
2121import java .util .LinkedHashMap ;
22+ import java .util .List ;
2223import java .util .Map ;
23- import java .util .function .Function ;
24- import java .util .stream .Collectors ;
2524
2625import jakarta .servlet .ServletException ;
2726import jakarta .servlet .http .HttpServletRequest ;
3534import org .springframework .security .config .Customizer ;
3635import org .springframework .security .config .annotation .web .HttpSecurityBuilder ;
3736import org .springframework .security .config .annotation .web .builders .HttpSecurity ;
38- import org .springframework .security .core .Authentication ;
3937import org .springframework .security .core .AuthenticationException ;
4038import 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 ;
4439import org .springframework .security .web .AuthenticationEntryPoint ;
45- import org .springframework .security .web .FormPostRedirectStrategy ;
46- import org .springframework .security .web .RedirectStrategy ;
4740import org .springframework .security .web .access .AccessDeniedHandler ;
4841import org .springframework .security .web .access .AccessDeniedHandlerImpl ;
4942import org .springframework .security .web .access .ExceptionTranslationFilter ;
5043import org .springframework .security .web .access .RequestMatcherDelegatingAccessDeniedHandler ;
5144import org .springframework .security .web .authentication .DelegatingAuthenticationEntryPoint ;
5245import 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 ;
5646import org .springframework .security .web .savedrequest .HttpSessionRequestCache ;
47+ import org .springframework .security .web .savedrequest .NullRequestCache ;
5748import org .springframework .security .web .savedrequest .RequestCache ;
49+ import org .springframework .security .web .util .ThrowableAnalyzer ;
50+ import org .springframework .security .web .util .matcher .AnyRequestMatcher ;
5851import org .springframework .security .web .util .matcher .RequestMatcher ;
5952import 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 }
0 commit comments