Expected Behavior The AccessDeniedHandler should be able to handle all AccessDeniedException.

Current Behavior The AccessDeniedException thrown from the AuthorizationManagerBeforeMethodInterceptor does not appear to have been processed by the ExceptionTranslationFilter. Is this normal, a point that needs enhancement, or a bug? I am not sure. Please help me with my doubts, thank you.

Context As shown in the figure, I correctly configured the AccessDeniedHandler, but the AccessDeniedException thrown due to using @ PreAuthorize will not be processed by it. QQ截图20230331154029

Comment From: jzheaux

Hi, @insight720. Instead of a screenshot, will you please provide a minimal sample application? This will help get your issue addressed faster.

Comment From: insight720

Sure. @jzheaux

My application context info

Oracle JDK 17

Window 11

Spring Boot 3.0.3 (which includes Spring Security 6.0.2)

Key Code

  1. Simplified SpringSecurityConfig
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // use Method Security
public class SpringSecurityConfig {
    @Configuration
    @RequiredArgsConstructor
    public static class SecurityFilterChainConfig {
        // autowired
        private final AccessDeniedHandler accessDeniedHandler;
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) {
            // accessDeniedHandler is configured
            http.exceptionHandling()
                    .authenticationEntryPoint(authenticationEntryPoint)
                    .accessDeniedHandler(accessDeniedHandler);
            return http.build();
        }
}

  1. Normal AccessDeniedHandlerImpl

java @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { // nothing important } }

  1. Use of @PreAuthorize
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @PutMapping("/authority")
    public Result<Void> modifyAccountAuthority(@Valid @RequestBody AccountAuthorityDTO accountAuthorityDTO) {
        userAccountService.updateAccountAuthority(accountAuthorityDTO);
        return ResultUtils.success();
    }
  1. A user without ROLE_ADMIN authority requests this method, then an AccessDeniedException is thrown by Spring Security, but can't catch by AccessDeniedHandlerImpl.

My question is, is this situation normal? Or is this a BUG?

Comment From: imaxkhan

hi i have exactly same problem. is there any way?

Comment From: insight720

@imaxkhan I am waiting for reply, and I have not further studied this issue at the moment. You can try whether @ControllerAdvice can catch exceptions and handle them, but I haven't tried that before. @jzheaux Could you please take a look at the question raised by this issue again? Thank you very much!

Comment From: Drophoff

Hi,

i have the same failure with Spring Boot 3.0.5 and Spring Security 6.0.2.

Yes, the exception can be handled with @ControllerAdvice, but we cannot distinguish between authentication or authorization errors, which is the case with the AccessDeniedHandler.

Furthermore the API to register an dedicated AccessDeniedHandler within the HttpSecurity is not working as documented and the ExceptionTranslationFilter, which has the aim to handle this kind of failure gets never called.

Comment From: HabeebCycle

Are we triaging this issue? Spring Security 6.0.2

Comment From: Drophoff

I updated the dependencies to Spring Boot 3.1.2 and Spring Security 6.1.2 and the failure is still present.

Comment From: marcusdacoregio

Hi everyone, if you clone my sample and perform the request http :8080/ -a user:password or if I open the browser and provide the user:password credentials, I get the proper 418 status code. Can you elaborate more on how to simulate the problem? Feel free to use my sample.

Comment From: Drophoff

I compared my configuration with the above provided one and found a configuration failure on my side. I would like to apologize for the inconvenience.

I have defined a @ControlledAdvice, which handled the AccessDeniedException and due to that the exception never reached the DispacherServlet nor the ExceptionTranslationFilter.

Thus, my error message is invalid and no longer valid.

Comment From: jzheaux

Thanks for the update, @Drophoff, and I'm glad you and @marcusdacoregio were able to sort things out.

Comment From: Rei-Nicolau-o-Grande

I imported the exceptions from Spring Security and it worked.

@ExceptionHandler({
            org.springframework.security.authorization.AuthorizationDeniedException.class, // Import Spring Security
            org.springframework.security.access.AccessDeniedException.class, // Import Spring Security
            AccessDeniedException.class, // My Custom Excepition
    })
    public ResponseEntity<ApiErrorDto> handlerForbiddenException(HttpServletRequest request,
                                                                   RuntimeException ex) {
        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)
                .contentType(MediaType.APPLICATION_JSON)
                .body(new ApiErrorDto(
                        LocalDateTime.now(),
                        request.getRequestURI(),
                        request.getMethod(),
                        HttpStatus.FORBIDDEN.value(),
                        HttpStatus.FORBIDDEN.getReasonPhrase(),
                        ex.getMessage(),
                        null,
                        ex.getClass()
                ));
    }

SecurityConfig does not need exceptionHandling to work.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Value("${jwt.public.key}")
    private RSAPublicKey publicKey;

    @Value("${jwt.private.key}")
    private RSAPrivateKey privateKey;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers(HttpMethod.POST, "/api/v1/token/login").permitAll()
                    .requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
                    .requestMatchers(DOCUMENTATION_OPENAPI).permitAll()
                    .anyRequest().authenticated())
            .csrf(csrf -> csrf.disable())
            .oauth2ResourceServer(
                    oauth2 -> oauth2.jwt(jwt -> jwt
                            .jwtAuthenticationConverter(jwtMyAuthenticationConverter())))
            .sessionManagement(
                    session -> session
                            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//            .exceptionHandling(
//                    exceptionHandling -> exceptionHandling
//                            .accessDeniedHandler(new CustomAccessDeniedHandler()))
        ;

        return http.build();
    }

Response Spring Security AccessDeniedHandler cannot handle exception thrown from AuthorizationManagerBeforeMethodInterceptor

Comment From: Rei-Nicolau-o-Grande

Now if you want to customize the response error message.

My DTO

public record ApiErrorDto(

        @JsonFormat(pattern="dd-MM-yyyy HH:mm:ss")
        LocalDateTime timestamp,

        String path,

        String method,

        Integer status,

        String error,

        String message,

        @JsonInclude(JsonInclude.Include.NON_NULL)
        Map<String, String> fields,

        @JsonInclude(JsonInclude.Include.NON_NULL)
        Object stakeTrace
) {
    public ApiErrorDto(LocalDateTime timestamp, String path, String method, Integer status, String error,
                       String message) {
        this(timestamp, path, method, status, error, message, null, null);
    }

    public ApiErrorDto(LocalDateTime timestamp, String path, String method, Integer status, String error,
                       String message, Map<String, String> fields, Object stakeTrace) {
        this.timestamp = timestamp;
        this.path = path;
        this.method = method;
        this.status = status;
        this.error = error;
        this.message = message;
        this.fields = fields;
        this.stakeTrace = stakeTrace;
    }
}

Create the CustomAccessDeniedHandler class and implement import org.springframework.security.web.access.AccessDeniedHandler;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    public CustomAccessDeniedHandler() {
        this.objectMapper = new ObjectMapper();
        this.objectMapper.registerModule(new JavaTimeModule());
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Acesso negado! component customizado | "
//                + accessDeniedException.getMessage());

        ApiErrorDto apiErrorDto = new ApiErrorDto(
                LocalDateTime.now(),
                request.getRequestURI(),
                request.getMethod(),
                HttpServletResponse.SC_FORBIDDEN,
                HttpStatus.FORBIDDEN.getReasonPhrase(),
                "Acesso negado! CustomAccessDeniedHandler",
                null,
                null
        );

        response.getWriter().write(objectMapper.writeValueAsString(apiErrorDto));
    }
}

Now, in SecurityConfig, you need to add exceptionHandling.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Value("${jwt.public.key}")
    private RSAPublicKey publicKey;

    @Value("${jwt.private.key}")
    private RSAPrivateKey privateKey;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers(HttpMethod.POST, "/api/v1/token/login").permitAll()
                    .requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
                    .requestMatchers(DOCUMENTATION_OPENAPI).permitAll()
                    .anyRequest().authenticated())
            .csrf(csrf -> csrf.disable())
            .oauth2ResourceServer(
                    oauth2 -> oauth2.jwt(jwt -> jwt
                            .jwtAuthenticationConverter(jwtMyAuthenticationConverter())))
            .sessionManagement(
                    session -> session
                            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling(
                    exceptionHandling -> exceptionHandling
                            .accessDeniedHandler(new CustomAccessDeniedHandler()))
        ;

        return http.build();
    }

Now ExceptionHandler removes AccessDeniedException.class and AuthorizationDeniedException.class.

    @ExceptionHandler({
            AccessDeniedException.class, // My Exception
    })
    public ResponseEntity<ApiErrorDto> handlerForbiddenException(HttpServletRequest request,
                                                                   RuntimeException ex) {
        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)
                .contentType(MediaType.APPLICATION_JSON)
                .body(new ApiErrorDto(
                        LocalDateTime.now(),
                        request.getRequestURI(),
                        request.getMethod(),
                        HttpStatus.FORBIDDEN.value(),
                        HttpStatus.FORBIDDEN.getReasonPhrase(),
                        ex.getMessage(),
                        null,
                        ex.getClass()
                ));
    }

Response Spring Security AccessDeniedHandler cannot handle exception thrown from AuthorizationManagerBeforeMethodInterceptor