javaspring-bootspring-securityspring-security-rest

Spring security exception handling custom response


How is it possible to return a json instead a html?

I got:

<!doctype html>
<html lang="en">

<head>
    <title>HTTP Status 401 – Unauthorized</title>
    <style type="text/css">
        body {
            font-family: Tahoma, Arial, sans-serif;
        }

        h1,
        h2,
        h3,
        b {
            color: white;
            background-color: #525D76;
        }

        h1 {
            font-size: 22px;
        }

        h2 {
            font-size: 16px;
        }

        h3 {
            font-size: 14px;
        }

        p {
            font-size: 12px;
        }

        a {
            color: black;
        }

        .line {
            height: 1px;
            background-color: #525D76;
            border: none;
        }
    </style>
</head>

<body>
    <h1>HTTP Status 401 – Unauthorized</h1>
</body>

</html>

i need something like this:

{
    "errors": [
        {
            "status": "401",
            "title": "UNAUTHORIZED",
            "detail": "xyz ..."
        }
    ]
}

My Adapter:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
   @Override
   protected void configure(HttpSecurity httpSecurity) throws Exception
   {
      // @formatter:off
      httpSecurity
               .csrf()
               .disable()
               .authorizeRequests()
               .antMatchers(HttpMethod.GET).permitAll()
               .anyRequest()
               .authenticated()
               .and()
               .httpBasic()
               .and()
               .exceptionHandling()
               .authenticationEntryPoint(new CustomAuthenticationEntryPoint());
      // @formatter:on
   }
}

The CustomAuthenticationEntryPoint

@Component
class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException
   {
      Collection<String> authorities = response.getHeaders("Authorization");
      response.addHeader("access_denied_reason", "authentication_required");
      response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
   }
}

Solution

  • Pragmatically we can print/write to response[.getWriter()] within our entry point, like:

    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    //...
    private static AuthenticationEntryPoint authenticationEntryPoint() {
      return (request, response, authException) -> {
        response.addHeader( // identic/similar to "basic" entry point
            "WWW-Authenticate", "Basic realm=\"Realm\""
        );
        // subtle difference to "basic" entry point :
        // better/looks like:
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        // response.addHeader...
        response.setStatus(HttpStatus.UNAUTHORIZED.value() /*, no message!(?) */);
        // "print" custom to "response" (with String#format, jackson, gson... (templating fw...)):
        response.getWriter().format("""
          {
            "errors":[
              {
                "status": %d,
                "title": "%s",
                "detail": "%s"
              }
            ]
          }
          """,
          HttpStatus.UNAUTHORIZED.value(),
          HttpStatus.UNAUTHORIZED.name(),
          authException.getMessage() // or whatever message/params/format we see fit
        );
      };
    }
    

    BasicAuthenticationEntryPoint@github

    Then we can pass a test like:

    package com.example.security.custom.entrypoint;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.test.web.servlet.MockMvc;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    @WebMvcTest
    class SecurityCustomEntrypointApplicationTests {
    
      @Autowired
      private MockMvc mvc;
    
      @Test
      void testUnauthorized() throws Exception {
        mvc
            .perform(post("/somewhere")) // no (basic) credentials(!), assuming no 404 :)
            .andDo(print())
            .andExpectAll(
                status().isUnauthorized(),
                header().exists("WWW-Authenticate"),
                jsonPath("$.errors[0].detail").exists(),
                jsonPath("$.errors[0].title").value("UNAUTHORIZED"),
                jsonPath("$.errors[0].status").value(401) // , ...
            );
      }
    }
    
    

    To make it work for basic authetication and "wrong credentials" see also: https://stackoverflow.com/a/74547059/592355 .

    Dup/Related: