Skip to content
Advertisement

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

Advertisement

Answer

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:

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement