AccessDeniedHandler

A JSON 403

@Component
public class JsonAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper mapper;

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

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException ex) throws IOException {
        String message = ex instanceof CsrfException
                ? "Invalid or missing CSRF token"
                : "You do not have permission to access this resource";
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        mapper.writeValue(response.getOutputStream(),
                new ApiError(403, "Forbidden", message, request.getRequestURI()));
    }
}

Note: Missing or invalid CSRF tokens also arrive here (CsrfException is an AccessDeniedException), which is why a bad CSRF token gives 403, not 401.

Registering Both Handlers

@Bean
SecurityFilterChain api(HttpSecurity http,
                        JsonAuthenticationEntryPoint entryPoint,
                        JsonAccessDeniedHandler deniedHandler) throws Exception {
    http
        // ... cors, csrf, session, authorizeHttpRequests, JWT filter as before
        .exceptionHandling(ex -> ex
            .authenticationEntryPoint(entryPoint)
            .accessDeniedHandler(deniedHandler));
    return http.build();
}

Example

curl -i localhost:8080/api/admin/users -H "Authorization: Bearer $ASHA_TOKEN"

Example output

HTTP/1.1 403 Forbidden
Content-Type: application/json
{"status":403,"error":"Forbidden","message":"You do not have permission to access this resource","path":"/api/admin/users"}