30thh
30thh

Reputation: 11386

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?

Upvotes: 13

Views: 10067

Answers (3)

sandeep pandey
sandeep pandey

Reputation: 350

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.

Upvotes: 5

Mikita Harbacheuski
Mikita Harbacheuski

Reputation: 2253

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);
            }
        }
    };
}

Upvotes: 2

Ammar
Ammar

Reputation: 4024

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.

Upvotes: 1

Related Questions