Custom Error message with @Preauthorize and @@ControllerAdvice
Asked Answered
C

4

17

We are using spring and spring-security-3.2. Recently We are adding annotations @PreAuthorize to RestAPIs(earlier it was URL based).

     @PreAuthorize("hasPermission('salesorder','ViewSalesOrder')")
  @RequestMapping(value = "/restapi/salesorders/", method = RequestMethod.GET)
  public ModelAndView getSalesOrders(){}

We already have Global exception handler which annotated with - @ControllerAdvice and custom PermissionEvaluator in place, everything works fine except the error message.

Lets say some user is accessing API At moment without having 'ViewSalesOrder' permission then spring by default throws the exception 'Access is denied',but didn't tell which permission is missing (Its our requirement to mention which permission is missing).

Is it possible to throw an exception which also include the permission name, so final error message should be look like "Access is denied, you need ViewSalesOrder permission"(here permission name should be from @PreAuthorize annotation)?

Please note that we have 100 such restAPI in place so generic solution will be highly appreciated.

Cresida answered 12/7, 2017 at 14:7 Comment(1)
what solution did you go with? I still cant find a way to set custom error messageAlphard
A
7

I have implemented the second possible solution mentioned by Mert Z. My solution works only for @PreAuthorize annotations used in the API layer (e.g. with @RequestMapping). I have registered a custom AccessDeniedHandler bean in which I get the value of the @PreAuthorize annotation of the forbidden API method and fills it into error message.

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private DispatcherServlet dispatcherServlet;

    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException,
            ServletException {
        if (!response.isCommitted()) {
            List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
            if (handlerMappings != null) {
                HandlerExecutionChain handler = null;
                for (HandlerMapping handlerMapping : handlerMappings) {
                    try {
                        handler = handlerMapping.getHandler(request);
                    } catch (Exception e) {}
                    if (handler != null)
                        break;
                }
                if (handler != null && handler.getHandler() instanceof HandlerMethod) {
                    HandlerMethod method = (HandlerMethod) handler.getHandler();
                    PreAuthorize methodAnnotation = method.getMethodAnnotation(PreAuthorize.class);
                    if (methodAnnotation != null) {
                        response.sendError(HttpStatus.FORBIDDEN.value(),
                                "Authorization condition not met: " + methodAnnotation.value());
                        return;
                    }
                }
            }
            response.sendError(HttpStatus.FORBIDDEN.value(),
                    HttpStatus.FORBIDDEN.getReasonPhrase());
        }
    }

    @Inject
    public void setDispatcherServlet(DispatcherServlet dispatcherServlet) {
        this.dispatcherServlet = dispatcherServlet;
    }
}

The handler is registered in WebSecurityConfigurerAdapter:

@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
@EnableWebSecurity
public abstract class BaseSecurityInitializer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
        ...
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }
}

Beware that if there is also a global resource exception handler with @ControllerAdvice the CustomAccessDeniedHandler won't be executed. I solved this by rethrowing the exception in the global handler (as advised here https://github.com/spring-projects/spring-security/issues/6908):

@ControllerAdvice
public class ResourceExceptionHandler {
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
        log.info(e.toString());
        throw e;
    }
}
Alrich answered 15/11, 2020 at 15:11 Comment(1)
is there a way to do this where the PreAuthorize annotation in service layerEntophyte
M
6

There is no pretty way of achieving what you expect since PermissionEvaluator interface doesn't let you pass the missing permission along with the result of the evaluation.
In addition, AccessDecisionManager decides on the final authorization with respect to the votes of the AccessDecisionVoter instances, one of which is PreInvocationAuthorizationAdviceVoter which votes with respect to the evaluation of @PreAuthorize value.

Long story short, PreInvocationAuthorizationAdviceVoter votes against the request (giving the request –1 point) when your custom PermissionEvaluator returns false to hasPermission call. As you see there is no way to propagate the cause of the failure in this flow.

On the other hand, you may try some workarounds to achieve what you want.

One way can be to throw an exception within your custom PermissionEvaluator when permission check fails. You can use this exception to propagate the missing permission to your global exception handler. There, you can pass the missing permission to your message descriptors as a parameter. Beware that this will halt execution process of AccessDecisionManager which means successive voters will not be executed (defaults are RoleVoter and AuthenticatedVoter). You should be careful if you choose to go down this path.

Another safer but clumsier way can be to implement a custom AccessDeniedHandler and customize the error message before responding with 403. AccessDeniedHandler provides you current HttpServletRequest which can be used to retrieve the request URI. However, bad news in this case is, you need a URI to permission mapping in order to locate the missing permission.

Milk answered 19/2, 2018 at 21:18 Comment(0)
G
2

You can throw an org.springframework.security.access.AccessDeniedException from a method that was called inside an EL-Expression:

@PreAuthorize("@myBean.myMethod(#myRequestParameter)")
Greenwald answered 29/3, 2022 at 9:25 Comment(0)
H
0

Ideally, the @PreAuthorize annotation should be supporting String message(); in addition to the SpEl value. But, for whatever reason, it does not. Most of the suggestions here seem unnecessarily cumbersome and elaborate. As @lathspell has suggested, the simplest way to provide your own error message - along with any custom access validation logic - would be to add a simple method that performs the check and throws the AccessDeniedException in case the check fails, and then reference that method in the SpEl expression. Here's an example:

@RestController
@RequiredArgsConstructor // if you use lombok
public class OrderController {  
    
    private final OrderService orderService;  
    ...
    
    @GetMapping(value = "/salesorders", produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("@orderController.hasPermissionToSeeOrders(#someArgOfThisMethod)")
    public Page<OrderDto> getSalesOrders(
                          // someArgOfThisMethod here, perhaps HttpRequest, @PathVariable, @RequestParam, etc. 
                          int pageIndex, int pageSize, String sortBy, String sortOrder) {
        Pageable pageRequest = PageRequest.of(pageIndex, pageSize, Sort.Direction.fromString(sortOrder), sortBy);
        return ordersService.retrieveSalesOrders(..., pageRequest);
    }

    public static Boolean hasPermissionToSeeOrders(SomeArgOfTheTargetMethod argToEvaluate) {
        //check eligibility to perform the operation based on some data from the incoming objects (argToEvaluate)
        if (condition fails) {
            throw new AccessDeniedException("Your message");
        }
        return true;
    }
Harmonics answered 18/8, 2022 at 14:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.