Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MVC (or Spring Boot). Custom JSON response for security related exceptions like 401 Unauthorized or 403 Forbidden)

I am developing a REST service. It uses JSON and must return some predefined JSON object in case of a problems. The default Spring response looks like this:

{
  "timestamp": 1512578593776,
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/swagger-ui.html"
}

I want to replace this default JSON with an own one (with a stacktrace and additional exception related information).

Spring provides a handy way to overwrite default behavior. One should define a @RestControllerAdvice bean with a custom exception handler. Like this

@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(value = {Exception.class})
  public ResponseEntity<ExceptionResponse> unknownException(Exception ex) {
    ExceptionResponse resp = new ExceptionResponse(ex, level); // my custom response object
    return new ResponseEntity<ExceptionResponse>(resp, resp.getStatus());
  }
  @ExceptionHandler(value = {AuthenticationException.class})
  public ResponseEntity<ExceptionResponse> authenticationException(AuthenticationExceptionex) {
      // WON'T WORK
  }
}

The custom ExceptionResponse object will be then converted to JSON by Spring using a special message converter.

THE PROBLEM IS, that security exceptions like InsufficientAuthenticationExceptioncannot be intercepted by a method annotated as @ExceptionHandler. This sort of exceptions happens before the Spring MVC dispatcher servlet were entered and all the MVC handlers were initialized.

It is possible to intercept this exception using a custom filter and build an own JSON serialization from scratch. In this case one would get a code which is completely independent from the rest of the Spring MVC infrastructure. It is not good.

The solution I found seems to work, but it looks crazy.

@Configuration
public class CustomSecurityConfiguration extends 
WebSecurityConfigurerAdapter {

@Autowired
protected RequestMappingHandlerAdapter requestMappingHandlerAdapter;

@Autowired
protected GlobalExceptionHandler exceptionHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()
        .anyRequest()
        .fullyAuthenticated();

    http.exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint());
}

public AuthenticationEntryPoint authenticationEntryPoint() {
    return new AuthenticationEntryPoint() {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException authException) throws IOException, ServletException {

            try {
                ResponseEntity<ExceptionResponse> objResponse = exceptionHandler.authenticationException(authException);

                Method unknownException = exceptionHandler.getClass().getMethod("authenticationException", AuthenticationException.class);

                HandlerMethod handlerMethod = new HandlerMethod(exceptionHandler, unknownException);

                MethodParameter returnType = handlerMethod.getReturnValueType(objResponse);

                ModelAndViewContainer mvc = new ModelAndViewContainer(); // not really used here.

                List<HttpMessageConverter<?>> mconverters = requestMappingHandlerAdapter.getMessageConverters();

                DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);

                HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(mconverters);

                processor.handleReturnValue(objResponse, returnType, mvc, webRequest);
            } catch (IOException e) {
                throw e;
            } catch (RuntimeException e) {
                throw e;                    
            } catch (Exception e) {
                throw new ServletException(e);
            }
        }
    };
}

Is there a way to use the Spring serialization pipe (with Spring build in message converters, MIME format negotiation etc.) which looks better than this?

like image 599
30thh Avatar asked Dec 06 '17 17:12

30thh


3 Answers

Create Bean Class

@Component public class AuthenticationExceptionHandler implements AuthenticationEntryPoint, Serializable

and override commence() method and create a json response using object mapper like the following example

 ObjectMapper mapper = new ObjectMapper();
 String responseMsg = mapper.writeValueAsString(responseObject);
 response.getWriter().write(responseMsg);

and @Autowire AuthenticationExceptionHandler in SecurityConfiguration class and in configure(HttpSecurity http) method add below lines

        http.exceptionHandling()
            .authenticationEntryPoint(authenticationExceptionHandler) 

This way, you should be able to send custom json response 401 /403. Same as above, you may use AccessDeniedHandler. Let us know if this helped you solve your problem.

like image 197
sandeep pandey Avatar answered Sep 22 '22 15:09

sandeep pandey


Spring provide out-of-box support for exception handling via AccessDeniedHandler, which can be utilized by following below steps to achieve a custom JSON response in case of an AccessDeniedException i.e. HTTP 403

Implement a custom handler something like below

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exc)
        throws IOException, ServletException {
        System.out.println("Access denied .. ");
        // do something
        response.sendRedirect("/deny");
    }
}

Next create a bean of this handler in config and supply it to spring security exception handler (Important note - ensure to exclude /deny from authentication else request will infinitely keep on resolving error)

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

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors().and()
            // some other configuration
            .antMatchers("/deny").permitAll()
            .and()
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler());
}

Next in the controller class write a handler corresponding to /deny and simply throw a new instance of SomeException (or any other Exception as suited) which will be intercepted in @RestControllerAdvice respective handler.

@GetMapping("/deny")
public void accessDenied(){
    throw new SomeException("User is not authorized");
}

Let know in comments if any more information is required.

like image 35
Bond - Java Bond Avatar answered Sep 25 '22 15:09

Bond - Java Bond


I think the next configuration should work, please try it.

@Autowired
private HandlerExceptionResolver handlerExceptionResolver;

public AuthenticationEntryPoint authenticationEntryPoint() {
    return new AuthenticationEntryPoint() {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException {

            try {
                handlerExceptionResolver.resolveException(request, response, null, authException);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new ServletException(e);
            }
        }
    };
}
like image 20
Nikita Gorbachevski Avatar answered Sep 24 '22 15:09

Nikita Gorbachevski