CodeGym /Courses /Spring Security /EntryPoint: 401 JSON

EntryPoint: 401 JSON

Spring Security
Level 17, Lesson 2
Available

1. Introduction

The problem is already visible: a REST endpoint without authentication should not send the client to /login or feed it HTML. And we have already separated the difference between 401 and 403: right now we are interested only in the first branch — there is no successful authentication for the current request.

So what we need is not an if in the controller, but a component in the security layer that will fire even earlier in the filter chain and return a proper 401 in JSON. Now we will assemble the first working version of this branch: without extracting shared code, but with the most transparent mechanics possible.

2. Mental model: AuthenticationEntryPoint

AuthenticationEntryPoint is the point where Spring Security arrives when a request runs into a lack of authentication. It does not check roles and does not decide 403; its job is simpler: answer the question "who are you anyway?" so the client understands that authentication is required first.

In short, the chain here is this: the security layer sees an anonymous request to a protected endpoint, ExceptionTranslationFilter intercepts that situation and calls the entry point. 403 has a separate branch through AccessDeniedHandler, and here we keep the focus only on the 401 branch.

Mini call diagram for AuthenticationEntryPoint

To avoid guessing "who actually calls it", it is useful to have a picture. At the application level (without diving into the whole filter catalog), it looks roughly like this:

flowchart TD
    A[HTTP request: GET /api/me] --> B[Security Filter Chain]
    B --> C["Access check: endpoint requires authenticated()"]
    C -->|anonymous user| D[ExceptionTranslationFilter]
    D --> E["AuthenticationEntryPoint.commence(...)"]
    E --> F[HTTP 401 + JSON body]

We do not need to know all the internals of ExceptionTranslationFilter at the source-code level, but the key idea is this: there is a filter that intercepts security exceptions and chooses how to respond. If the problem is a lack of authentication, it calls the entry point.

3. Implementation: RestAuthenticationEntryPoint

Now we will take the most practical step possible: write a component that forms a 401 response and the response body in JSON. Our goal is for any API client (Postman, curl, a mobile app, another backend service) to rely on three things: a correct status, Content-Type: application/json, and a predictable body.

We will use the most direct working form: a separate DTO and writing JSON directly inside commence(...). That is enough to see the mechanics of the 401 branch without unnecessary abstraction.

Mini DTO for the response body

Let us start simple: we need an object that Jackson can serialize to JSON. For now this is a local DTO specifically for the 401 response, so record in Java 25 is perfect — short, clear, and it does not balloon into a pile of getters.

package com.example.securecontent.security.handler;

// Local DTO for the first working version of the 401 response
public record ApiSecurityError(
    int status,    // HTTP status (for example, 401)
    String error,  // Short error code (for example, UNAUTHORIZED)
    String message,// Human-readable message (without extra details)
    String path    // Request path where the error occurred
) {
}

Yes, the fields here are minimal: status, short error code, message, and path. That is enough for the client to "understand what happened" without manual JSON assembly and without unnecessary detail.

Implementing AuthenticationEntryPoint

Now we write the component. We will also assemble the response body right here: that makes it easy to see exactly what commence(...) does. We will inject ObjectMapper from Spring Boot — that is convenient because it is already configured (modules, dates, serialization settings) and you do not create a "random" mapper that behaves differently elsewhere in the project.

package com.example.securecontent.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

// First working version: instead of HTML/redirects, return JSON with 401
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    public RestAuthenticationEntryPoint(ObjectMapper objectMapper) {
        // Take ObjectMapper from the Spring Boot context so serialization is consistent across the project
        this.objectMapper = objectMapper;
    }
}

For now this is just a shell: Spring will see @Component, create a bean, and be able to use it in configuration.

Now let us add the commence(...) method. This is what will actually build the response.

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;

import java.io.IOException;

And the implementation:

@Override
public void commence(HttpServletRequest request,
                     HttpServletResponse response,
                     AuthenticationException ex) throws IOException {

    // 1) Explicitly set the HTTP status: no authentication -> 401
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    // 2) Explicitly tell the client that the body is JSON (otherwise some clients "guess" wrong)
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

    // 3) Build a predictable response body (without exposing internal details)
    var body = new ApiSecurityError(
        401,
        "UNAUTHORIZED",
        "Authentication is required",
        request.getRequestURI()
    );

    // 4) Serialize the object to JSON via ObjectMapper (not by hand as a string)
    objectMapper.writeValue(response.getOutputStream(), body);
}

Notice three things.

First, we set the 401 status manually. We do not "throw the exception onward", and we do not "hope Spring figures it out." We own the contract, and we respond.

Second, we explicitly set Content-Type. If you forget this, the client may try to interpret the response as text, HTML, or something else, and you get strange behavior on the UI or integration side.

Third, we write JSON through ObjectMapper, not through response.getWriter().println("{...}"). The latter works... until the first special character, until the first escaping requirement, until the first localization, and until the first change in structure. Writing JSON by hand is like building an airplane out of matches: amazing that it flies, but better not to do it.

4. Wiring it into SecurityFilterChain

One of the nicest things about Spring Security is when you understand that most of the "magic" is actually wired in with one line, you just need to put it in the right place. The entry point is configured through exceptionHandling in the HttpSecurity configuration.

Let us do that in our security configuration. Suppose we have a SecurityConfig class in the com.example.securecontent.security.config package.

This is the first working build: the 401 is now controlled centrally in the security layer, without redirects and without trying to fix everything in the controller.

package com.example.securecontent.security.config;

import com.example.securecontent.security.handler.RestAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
                                        RestAuthenticationEntryPoint entryPoint) throws Exception {
    // Important: this @Bean method must be inside a configuration class (for example, @Configuration)
    // Wire the first working 401 branch: when authentication is absent, we return our JSON, not a redirect/HTML
    http.exceptionHandling(ex -> ex.authenticationEntryPoint(entryPoint));

    return http.build();
}

This snippet by itself does not say which endpoints are protected. But it fixes something important: when Spring Security decides that authentication is needed, it will not redirect to a login page (if you have not left the default entry point in place), but will call our entry point, and we will return JSON.

To show "everything together" a bit more realistically, let us add minimal access rules.

http.authorizeHttpRequests(auth -> auth
    // Public endpoints: available without authentication
    .requestMatchers("/api/public/**", "/api/auth/register").permitAll()
    // Everything else: only for authenticated users
    .anyRequest().authenticated()
);

And together with the entry point:

http
  .authorizeHttpRequests(auth -> auth
      // Public endpoints: available without authentication
      .requestMatchers("/api/public/**", "/api/auth/register").permitAll()
      // Everything else: only for authenticated users
      .anyRequest().authenticated()
  )
  // If authentication is absent, return 401 in JSON (via our EntryPoint)
  .exceptionHandling(ex -> ex.authenticationEntryPoint(entryPoint));

Notice that we are not discussing whether formLogin or httpBasic is enabled. That is a separate topic (and you already had lectures about form login and HTTP Basic). AuthenticationEntryPoint is responsible specifically for the reaction when authentication is absent, regardless of how you planned to do authentication in the first place.

5. Checking it in Secure Content Platform API

Any security configuration without testing is like a seat belt that "is somewhere in the car". Technically it exists, but it only helps in a fantasy world. So let us check the symptoms: how does /api/me behave when the user is not authenticated?

Suppose we have a controller:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
class MeController {

    @GetMapping("/api/me")
    public Map<String, String> me() {
        // This endpoint is private: if authentication is missing, the EntryPoint should fire and return 401 JSON
        return Map.of("message", "private zone");
    }
}

Now we check without authentication:

# Request without a token/session: expect 401 from the security layer
curl -i http://localhost:8080/api/me

We expect something like this, approximately (simplified):

HTTP/1.1 401
Content-Type: application/json
...

{"status":401,"error":"UNAUTHORIZED","message":"Authentication is required","path":"/api/me"}

And that is already "REST-friendly": the client sees 401, sees JSON, and can easily handle it and show the user "please sign in", or initiate the login flow, or simply and honestly say "access denied".

A small but important discipline

Even if it seems like "well, it is already clear why 401 happened", the field path makes life much easier during debugging, especially when you have a Postman collection with 40 requests, some of them are parameterized, and you accidentally hit the wrong one.

6. Responsibility boundaries of AuthenticationEntryPoint

It is very easy to start expecting everything from the entry point: "let it fix login too, and authorization errors, and validation, and maybe brew coffee as well." But the right mental model here saves time and nerves.

AuthenticationEntryPoint is responsible for the reaction to the problem "there is no authentication". This is the place where you must make the API response predictable, but you must not turn it into a business error handler. It should not go to the database, assemble complex traceId values, pull in all user fields, and explain the internal security policy to the client.

The entry point also does not solve the 403 problem. If the user is already authenticated and tries to access /api/admin/**, the entry point should not "pretend this is 401". These are fundamentally different scenarios. And the next step of the day is an separate handler for forbidden scenarios.

Another boundary is "overly detailed errors". If in message you start writing "User is disabled" or "Account is locked", you may accidentally turn your API into an information desk for an attacker: they will try logins and learn which accounts exist and in what state. Even in a learning project, it is useful to get used to neutral wording right away.

7. Typical mistakes when configuring AuthenticationEntryPoint

Mistake #1: return 403 from the entry point "because access is forbidden".
The logic is understandable from a human point of view, but wrong in protocol terms: 403 means the user is known (authenticated), but not allowed. If you return 403 to an anonymous user, the client starts thinking "I am signed in, but to the wrong place", although in reality the user is not signed in at all.

Mistake #2: forget Content-Type and end up with "JSON that is not JSON".
Without Content-Type: application/json, many clients will behave strangely: some will show the body as plain text, some will try to parse it as HTML, and some (especially in a mobile SDK) will simply not find the right handler. In API contracts, small things are not small.

Mistake #3: write JSON by hand as a string.
Today you wrote "{\"error\":\"UNAUTHORIZED\"}", tomorrow you added a quote to the text, the day after that localization brought a newline character — and suddenly you are returning invalid JSON to the client. ObjectMapper exists for a reason.

Mistake #4: expose too much internal information.
The AuthenticationException may contain details internally that are useful in your logs but not useful externally. Do not return ex.getMessage() directly to the client. This is a common "convenient" practice that later turns into an unpleasant vulnerability and an unstable contract.

Mistake #5: try to solve the problem in the controller with try/catch.
If a request does not pass the security filters, your controller will not see it. That is why handling "no authentication" should live exactly where it happens — in the security layer, through AuthenticationEntryPoint.

1
Task
Spring Security, level 17, lesson 2
Locked
Custom `AuthenticationEntryPoint` for `/api/profile`
Custom `AuthenticationEntryPoint` for `/api/profile`
1
Task
Spring Security, level 17, lesson 2
Locked
One `AuthenticationEntryPoint` for multiple protected endpoints
One `AuthenticationEntryPoint` for multiple protected endpoints
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION