AuthenticationEntryPoint

A JSON 401

By default a stateless API returns an empty 401 (or redirects to a login page if form login is on). A REST client wants JSON. First a small error record shared by all handlers:

public record ApiError(int status, String error, String message, String path) {}
package in.shopease.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper mapper;

    public JsonAuthenticationEntryPoint(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException ex) throws IOException {
        String tokenError = (String) request.getAttribute(JwtAuthFilter.AUTH_ERROR);
        String message;
        if (tokenError != null) {
            message = tokenError;                                    // "Access token expired"
            response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\"");
        } else if (ex instanceof BadCredentialsException) {
            message = "Invalid email or password";
        } else {
            message = "Authentication required";
            response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer");
        }
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        mapper.writeValue(response.getOutputStream(),
                new ApiError(401, "Unauthorized", message, request.getRequestURI()));
    }
}

Responses

Example

No token, expired token:

curl -i localhost:8080/api/orders
curl -i localhost:8080/api/orders -H "Authorization: Bearer $EXPIRED_TOKEN"

Example output

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json
{"status":401,"error":"Unauthorized","message":"Authentication required","path":"/api/orders"}

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Content-Type: application/json
{"status":401,"error":"Unauthorized","message":"Access token expired","path":"/api/orders"}