skip to content

What is AuthorizationDeniedException in Spring Security 6, and how does it relate to AccessDeniedException and the AccessDeniedHandler?

level: seniorimportance: should knowfreq 30%

answer

  1. AuthorizationDeniedException extends AccessDeniedException
  2. AuthorizationManager-based, SS6 unified model
  3. carries AuthorizationResult
  4. @HandleAuthorizationDenied post-processing
  5. filter path → handler; MVC path → @ExceptionHandler

basics

~10 s

AuthorizationDeniedException is the exception the modern AuthorizationManager-based checks (like @PreAuthorize and AuthorizationFilter) throw when access is denied. It extends AccessDeniedException, so it flows through the same ExceptionTranslationFilter and AccessDeniedHandler to a 403.

solid answer

~40 s

In Spring Security 6 the authorization model is unified around AuthorizationManager. When a check denies access it throws AuthorizationDeniedException, which is a subclass of AccessDeniedException and carries the AuthorizationResult/decision. Because it IS an AccessDeniedException, everything built for AccessDeniedException still applies: if it propagates to the ExceptionTranslationFilter in the filter chain (e.g. from AuthorizationFilter enforcing authorizeHttpRequests), your AccessDeniedHandler turns it into a 403. For method security, @PreAuthorize failures also throw AuthorizationDeniedException, but thrown inside the controller invocation they're typically resolved by Spring MVC's exception handling rather than the filter-chain handler. The subclass exists so denials carry richer context and can be post-processed (e.g. @PreAuthorize with a fallback via @HandleAuthorizationDenied).

code

java · 21 lines
java
// Catch the base type so BOTH the legacy AccessDeniedException
// and the SS6 AuthorizationDeniedException subclass are handled.
@RestControllerAdvice
class SecurityExceptionAdvice {

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    Map<String, Object> onDenied(AccessDeniedException ex,
                                 HttpServletRequest req) {
        return Map.of(
            "status", 403,
            "error", "forbidden",
            "path", req.getRequestURI());
    }
}

// This @PreAuthorize failure throws AuthorizationDeniedException
// (a subclass of AccessDeniedException) inside the MVC dispatch.
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/report")
String report() { return "secret"; }

go deeper

for a junior

Just know it's the exception that means 'authenticated but denied' and leads to 403.

for a middle

Know it extends AccessDeniedException and therefore reuses the same handler wiring.

for a senior

Explain the AuthorizationManager unification, the two propagation paths, and why you may need both a handler and an @ExceptionHandler.

for a principal

Discuss @HandleAuthorizationDenied post-processing, typed AuthorizationResult, and designing consistent 403 behavior across filter and method security without duplicated envelopes.

**Background.** Spring Security 6 replaced the older voter/`AccessDecisionManager` machinery with `AuthorizationManager<T>` as the single authorization abstraction. Both URL security (`AuthorizationFilter` backing `authorizeHttpRequests`) and method security (`@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, `@PostFilter`) now go through `AuthorizationManager`. **The exception.** When an `AuthorizationManager` returns a denied decision, the enforcing code throws `org.springframework.security.authorization.AuthorizationDeniedException`. Crucially, **it extends `AccessDeniedException`** (from `org.springframework.security.access`). So it is a drop-in for everything that already handles `AccessDeniedException`: - The `ExceptionTranslationFilter` catches `AccessDeniedException` (and therefore its subclass) and routes to the `AccessDeniedHandler` (403) or `AuthenticationEntryPoint` (401 for anonymous). - A `@ExceptionHandler(AccessDeniedException.class)` in a `@ControllerAdvice` also matches it, because subclasses are matched. **What the subclass adds.** `AuthorizationDeniedException` carries the `AuthorizationResult` (the decision detail). This enables richer handling — for example, Spring Security 6.3+ can *post-process* a denied `@PreAuthorize`/`@PostAuthorize` result via a `@HandleAuthorizationDenied` deny handler that returns a masked/fallback value instead of throwing. That's only possible because the denial is a first-class typed result, not just a generic exception. **Two propagation paths (the senior distinction).** 1. **Filter-chain authorization** (`authorizeHttpRequests`): `AuthorizationFilter` runs *before* the `DispatcherServlet`. Its `AuthorizationDeniedException` propagates up to `ExceptionTranslationFilter` → your **`AccessDeniedHandler`** produces the 403. 2. **Method security** (`@PreAuthorize` on a controller/service): the exception is thrown *during* the controller method invocation, i.e. inside the `DispatcherServlet`. Spring MVC's `HandlerExceptionResolver` catches it first, so a **`@ExceptionHandler`/`@ControllerAdvice`** handles it and, unless it rethrows past the filter, the `AccessDeniedHandler` never runs. (Spring Boot's default `ResponseStatusExceptionResolver` won't map it automatically because `AccessDeniedException` has no `@ResponseStatus`; without an advice you can get a 500. Spring Security's `AuthorizationManagerBeforeMethodInterceptor` and Boot error handling usually still surface 403, but for a *custom* body you need the advice.) **Practical implication.** To return a uniform 403 envelope regardless of where the denial originates, wire **both**: the `AccessDeniedHandler` (filter path) and a `@ControllerAdvice @ExceptionHandler(AccessDeniedException.class)` returning `HttpStatus.FORBIDDEN` (method-security path). Both catch `AuthorizationDeniedException` via the superclass. **Gotcha.** Don't try to catch `AuthorizationDeniedException` specifically and forget the base type — catch/handle `AccessDeniedException` so you cover legacy throwers and the new subclass together.

  • If AuthorizationDeniedException extends AccessDeniedException, why does the class even exist?
    To carry the AuthorizationResult/decision so denials are typed and richer than a generic exception — enabling features like @HandleAuthorizationDenied post-processing that returns a masked or fallback value instead of throwing, and better diagnostics. It stays a subclass so existing 403 handling keeps working.
  • For a @PreAuthorize denial on a controller method, does your filter-chain AccessDeniedHandler run?
    Usually not — the exception is thrown inside the DispatcherServlet during method invocation, so Spring MVC's HandlerExceptionResolver / @ExceptionHandler catches it first. The filter-chain AccessDeniedHandler only handles exceptions raised in the filter chain (e.g. authorizeHttpRequests via AuthorizationFilter).

saying these in an interview costs you the question

  • Claiming AuthorizationDeniedException is unrelated to AccessDeniedHandler
  • Thinking it does NOT extend AccessDeniedException
  • Assuming the filter-chain handler always catches @PreAuthorize denials
  • Saying SS6 still uses AccessDecisionManager/voters by default

context