Search code examples
javaspringauthenticationspring-securityerror-handling

How to take body of HttpServletResponse returned by one AuthenticationEntryPoint and modify it in SubClassEntryPoint


I am using an authentication library that provides its own Spring Security's AuthenticationEntryPoint to fill HttpServletResponse with data if authentication exceptions occurs. A response body might look for example like this:

{
    "errors": [
        {
            "identifier": "LIBRARY_001",
            "message": "Authentication token missing!",
            "solution": null,
            "reasons": []
        }
    ],
    "warnings": [],
    "metadata": null
}

However my company standard is to return a specific company-wide error code and message in case of an error. I could just implement my own EntryPoint and ignore the existence of the library's EntryPoint but I thought that a perfect solution would be to wrap the library's error and put it as a reason for my company-wide error. E.g.

{
    "errors": [
        {
            "identifier": "COMPANY_001",
            "message": "Authentication token is missing, but its company standard message!",
            "solution": null,
            "reasons": [
                {
                    "identifier": "LIBRARY_001",
                    "message": "Authentication token missing!"
                }
            ]
        }
    ],
    "warnings": [],
    "metadata": null
}

Now I don't know how to achieve this. So far I could think of 2 ways: 1st is I believe bad practice, 2nd is yet incomplete and seems like there should be an easier way to do it.

  1. Just copy most of the library's EntryPoints code to my code and add extra lines to achieve desired effect. This seems like a bad idea since it's code duplication of library code and you should be able to use the existing code.

  2. Create my own EntryPoint that inherits from library's EntryPoint and calls it using super.commence(req, res, ex) and modifies the result. However I'm strugling to deserialize the ExceptionResponse in order to modify it. Here is my idea so far:

     public class CompanyAuthenticationEntryPoint extends LibraryAuthenticationEntryPoint {
     (...)
     @Override
     public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception)
         throws IOException {
         ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
    
         super.commence(request, responseWrapper, exception);
         byte[] responseArray = responseWrapper.getContentAsByteArray();
         String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());
         // Below throws error due to lack of default constructor inside ExceptionResponse
         ExceptionResponse exceptionResponse2 = objectMapper.readValue(responseStr, ExceptionResponse.class);
    
         // Modify exceptionResponse2 i.e. put old error inside reasons[], put companys error code and identifier.
         // ..
         // not tested code below
         responseWrapper.copyBodyToResponse();
         response.getWriter().write(
             this.objectMapper.writeValueAsString(exceptionResponse2));
     }
     }
    

Does anyone have a better idea on how to solve this problem or can comment on my current ideas?


Solution

  • TL; DR;

    Use ContentCachingResponseWrapper

    Full answer:

    Ok so I kept on working on the 2nd solution and it turned out to give good results. First of all the issues I had with deserializing using any of these 2 approaches was due to imperfect implementation of ExcetionResponse, which was a company's in-house class. It was made by different team and at first I haven't noticed it. You will probably have no problems using either of these:

    ExceptionResponse exceptionResponse1 = objectMapper.readValue(responseStr, ExceptionResponse.class);
    ExceptionResponse exceptionResponse2 = objectMapper.readerForUpdating(emptyExceptionResponse).readValue(responseStrAlternated);
    

    However after discussions I eventually ended up returning a different class as error response and just attaching the previous error as string. I'm attaching my final solution. If you combine it with either of the 2 lines above it should be trivial to make code that assigns data of old error response to field "reason" or other field of the new error response

    @Override
    public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception)
            throws IOException {
        log.warn(MESSAGE_AUTHENTICATION_FAILED);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
    
        super.commence(request, responseWrapper, exception);
    
        String responseStr = new String(responseWrapper.getContentAsByteArray(), responseWrapper.getCharacterEncoding());
    
        NewErrorResponse newErrorResponse = errorResponseWithDetail(responseStr);
    
        response.setStatus(responseWrapper.getStatus());
        response.setContentType(responseWrapper.getContentType());
        response.getWriter().write(this.objectMapper.writeValueAsString(newErrorResponse));
    }