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 ;
3433import org .springframework .security .config .Customizer ;
3534import org .springframework .security .config .annotation .web .HttpSecurityBuilder ;
3635import org .springframework .security .config .annotation .web .builders .HttpSecurity ;
37- import org .springframework .security .core .Authentication ;
3836import org .springframework .security .core .AuthenticationException ;
3937import 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 ;
4338import org .springframework .security .web .AuthenticationEntryPoint ;
44- import org .springframework .security .web .FormPostRedirectStrategy ;
45- import org .springframework .security .web .RedirectStrategy ;
4639import org .springframework .security .web .access .AccessDeniedHandler ;
4740import org .springframework .security .web .access .AccessDeniedHandlerImpl ;
4841import org .springframework .security .web .access .ExceptionTranslationFilter ;
4942import org .springframework .security .web .access .RequestMatcherDelegatingAccessDeniedHandler ;
5043import org .springframework .security .web .authentication .DelegatingAuthenticationEntryPoint ;
5144import 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 ;
5545import org .springframework .security .web .savedrequest .HttpSessionRequestCache ;
46+ import org .springframework .security .web .savedrequest .NullRequestCache ;
5647import org .springframework .security .web .savedrequest .RequestCache ;
48+ import org .springframework .security .web .util .ThrowableAnalyzer ;
49+ import org .springframework .security .web .util .matcher .AnyRequestMatcher ;
5750import org .springframework .security .web .util .matcher .RequestMatcher ;
5851import 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 }
0 commit comments