Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get Spring-Security to return a 401 response as a JSON format?

I have a ReST API to an application with all controllers found under /api/, these all return JSON responses with a @ControllerAdvice which handles all exceptions to map to JSON formatted results.

This works great as of spring 4.0 @ControllerAdvice now supports matching on annotations. What I can't work out is how to return a JSON result for a 401 - Unauthenticated and 400 - Bad Request responses.

Instead Spring is simply returning the response to the container (tomcat) which renders this as HTML. How can I intercept this and render a JSON result using the same technique that my @ControllerAdvice is using.

security.xml

<bean id="xBasicAuthenticationEntryPoint"       class="com.example.security.XBasicAuthenticationEntryPoint">   <property name="realmName" value="com.example.unite"/> </bean> <security:http pattern="/api/**"                create-session="never"                use-expressions="true">   <security:http-basic entry-point-ref="xBasicAuthenticationEntryPoint"/>   <security:session-management />   <security:intercept-url pattern="/api/**" access="isAuthenticated()"/> </security:http> 

XBasicAuthenticationEntryPoint

public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {     @Override     public void commence(HttpServletRequest request,                          HttpServletResponse response,                          AuthenticationException authException)             throws IOException, ServletException {         HttpServletResponse httpResponse = (HttpServletResponse) response;         httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,                                authException.getMessage());     } } 

I can solve 401 by using the BasicAuthenticationEntryPoint to write directly to the output stream, but I'm not sure it's the best approach.

public class XBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {      private final ObjectMapper om;      @Autowired     public XBasicAuthenticationEntryPoint(ObjectMapper om) {         this.om = om;     }      @Override     public void commence(HttpServletRequest request,                          HttpServletResponse response,                          AuthenticationException authException)             throws IOException, ServletException {         HttpServletResponse httpResponse = (HttpServletResponse) response;         httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);         om.writeValue(httpResponse.getOutputStream(),                       new ApiError(request.getRequestURI(),                       HttpStatus.SC_UNAUTHORIZED,                       "You must sign in or provide basic authentication."));     }  } 

I am yet to figure out how to handle 400 though, I once tried a catch all controller which did work, but it seemed that sometimes it would have odd conflicting behaviour with other controllers that I don't want to revisit.

My ControllerAdvice implementation has a catch all which if spring throws any exception for bad request (400) it should in theory capture it, but it does not:

@ControllerAdvice(annotations = {RestController.class}) public class ApiControllerAdvisor {     @ExceptionHandler(Throwable.class)     public ResponseEntity<ApiError> exception(Throwable exception,                                               WebRequest request,                                               HttpServletRequest req) {         ApiError err = new ApiError(req.getRequestURI(), exception);         return new ResponseEntity<>(err, err.getStatus());     } } 
like image 216
Brett Ryan Avatar asked Apr 01 '14 10:04

Brett Ryan


People also ask

How do I return a JSON object to a REST web service?

Create RESTEasy Web Service to Produce JSON with @BadgerFish Now create a class whose methods will be exposed to the world as web service. Use JBoss @BadgerFish annotation that supports to return response as JSON. To return JSON as response we need to use media type as application/json.

Will Spring Security secures all the applications?

If Spring Security is on the classpath, Spring Boot automatically secures all HTTP endpoints with “basic” authentication. However, you can further customize the security settings. The first thing you need to do is add Spring Security to the classpath.

Which service is used by Spring Security to validate the user?

UserDetailsService. UserDetailsService is described as a core interface that loads user-specific data in the Spring documentation. In most use cases, authentication providers extract user identity information based on credentials from a database and then perform validation.


2 Answers

I actually found myself asking the very same question a few weeks ago - As Dirk pointed out in the comments, @ControllerAdvice will only kick in if an exception is thrown from within a controller method, so will inevitably not catch all things (I was actually trying to solve the case for a JSON response for a 404 error).

The solution I settled on, although not entirely pleasant (hopefully if you get a better answer I will change my approach) is handling the error mapping in the Web.xml - I added the following, which will override the tomcat default error pages with specific URL mappings:

<error-page>     <error-code>404</error-code>     <location>/errors/resourcenotfound</location> </error-page> <error-page>     <error-code>403</error-code>     <location>/errors/unauthorised</location> </error-page> <error-page>     <error-code>401</error-code>     <location>/errors/unauthorised</location> </error-page> 

Now, if any page returns a 404, it is handled by my error controller something like this:

@Controller @RequestMapping("/errors") public class ApplicationExceptionHandler {       @ResponseStatus(HttpStatus.NOT_FOUND)     @RequestMapping("resourcenotfound")     @ResponseBody     public JsonResponse resourceNotFound(HttpServletRequest request, Device device) throws Exception {         return new JsonResponse("ERROR", 404, "Resource Not Found");     }      @ResponseStatus(HttpStatus.UNAUTHORIZED)     @RequestMapping("unauthorised")     @ResponseBody     public JsonResponse unAuthorised(HttpServletRequest request, Device device) throws Exception {         return new JsonResponse("ERROR", 401, "Unauthorised Request");     } } 

It all still feels pretty grim - and of course is a global handling of errors - so if you didn't always want a 404 response to be json (if you were serving a normal webapp of the same application) then it doesnt work so well. But like I said, its what I settled on to get moving, here's hoping there is a nicer way!

like image 94
rhinds Avatar answered Oct 15 '22 10:10

rhinds


I have solved this by implementing my own version of HandlerExceptionResolver and subclassing DefaultHandlerExceptionResolver. It took a bit to work this out and you must override most methods, though the following has done exactly what I'm after.

Firstly the basics are to create an implementation.

public class CustomHandlerExceptionResolver extends DefaultHandlerExceptionResolver {     private final ObjectMapper om;     @Autowired     public CustomHandlerExceptionResolver(ObjectMapper om) {         this.om = om;         setOrder(HIGHEST_PRECEDENCE);     }     @Override     protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {         return request.getServletPath().startsWith("/api");     } } 

And now register it in your servlet context.

This now does nothing different, but will be the first HandlerExceptionResolver to be tried and will match any request starting with a context path of /api (note: you could make this a configurable parameter).

Next, we now can override any method that spring encounters errors on, there are 15 on my count.

What I have found is that I can write to the response directly and return an empty ModelAndView object or return null if my method did not eventually handle the fault which causes the next exception resolver to be tried.

As an example to handle situations where a request binding fault occurred I have done the following:

@Override protected ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {     ApiError ae = new ApiError(request.getRequestURI(), ex, HttpServletResponse.SC_BAD_REQUEST);     response.setStatus(ae.getStatusCode());     om.writeValue(response.getOutputStream(), ae);     return new ModelAndView(); } 

The only disadvantage here is that because I'm writing to the stream myself I'm not using any message converters to handle writing the response, so if I wanted to support XML and JSON API's at the same time it would not be possible, fortunately I'm only interested in supporting JSON, but this same technique could be enhanced to use view resolvers to determine what to render in etc.

If anyone has a better approach I'd still be interested in knowing how to deal with this.

like image 38
Brett Ryan Avatar answered Oct 15 '22 10:10

Brett Ryan