Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AccessDeniedHandler cannot handle exception thrown from AuthorizationManagerBeforeMethodInterceptor #12951

Closed
insight720 opened this issue Mar 31, 2023 · 12 comments
Assignees
Labels
in: core An issue in spring-security-core status: invalid An issue that we don't feel is valid

Comments

@insight720
Copy link

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

@insight720 insight720 added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Mar 31, 2023
@jzheaux
Copy link
Contributor

jzheaux commented Apr 11, 2023

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

@jzheaux jzheaux self-assigned this Apr 11, 2023
@jzheaux jzheaux added status: waiting-for-feedback We need additional information before we can continue in: core An issue in spring-security-core and removed status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Apr 11, 2023
@insight720
Copy link
Author

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

    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request,
                           HttpServletResponse response,
                           AccessDeniedException accessDeniedException) {
            // nothing important
        }
    }
  2. 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?

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 11, 2023
@imaxkhan
Copy link

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

@insight720
Copy link
Author

@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!

@Drophoff
Copy link

Drophoff commented May 9, 2023

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.

@HabeebCycle
Copy link

Are we triaging this issue? Spring Security 6.0.2

@Drophoff
Copy link

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

@marcusdacoregio
Copy link
Contributor

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.

@marcusdacoregio marcusdacoregio added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Oct 26, 2023
@Drophoff
Copy link

Drophoff commented Nov 1, 2023

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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Nov 1, 2023
@jzheaux
Copy link
Contributor

jzheaux commented Jan 23, 2024

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

@jzheaux jzheaux closed this as completed Jan 23, 2024
@jzheaux jzheaux added status: invalid An issue that we don't feel is valid and removed status: feedback-provided Feedback has been provided labels Jan 23, 2024
@Rei-Nicolau-o-Grande
Copy link

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
image

@Rei-Nicolau-o-Grande
Copy link

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
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core An issue in spring-security-core status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

8 participants