401 vs 403

Two Different Questions

StatusMeaningQuestion that failedShopEase exampleClient should
401 UnauthorizedNot authenticated"Who are you?"No token, expired token, bad signature, wrong passwordRefresh the token or show login
403 ForbiddenAuthenticated, but not allowed"May you do this?"Asha calls /api/admin/users; missing CSRF tokenShow "no permission"; logging in again will not help

Note: The name "401 Unauthorized" is historical and confusing; it really means unauthenticated. A 401 response should include a WWW-Authenticate header, e.g. Bearer error="invalid_token".

How Spring Decides

Both cases start as an exception. ExceptionTranslationFilter catches it and picks the handler:

AuthenticationException ------------------------------> AuthenticationEntryPoint -> 401
AccessDeniedException --+-- user is anonymous ----------> AuthenticationEntryPoint -> 401
                        +-- user is authenticated ------> AccessDeniedHandler      -> 403

Example

The same decision in plain Java:

import java.util.Set;

public class Main {
    record User(String name, Set<String> authorities) {}

    static String call(User user, String requiredRole) {
        try {
            if (user == null && requiredRole != null) throw new SecurityException("access denied");
            if (requiredRole != null && !user.authorities().contains(requiredRole)) {
                throw new SecurityException("access denied");
            }
            return "200 OK";
        } catch (SecurityException e) {
            // ExceptionTranslationFilter logic
            return user == null
                ? "401 Unauthorized (entry point: please log in)"
                : "403 Forbidden    (access denied handler: " + user.name() + " lacks " + requiredRole + ")";
        }
    }

    public static void main(String[] args) {
        User asha = new User("asha", Set.of("ROLE_CUSTOMER"));
        User meera = new User("meera", Set.of("ROLE_ADMIN"));
        System.out.println("GET /api/products     anonymous -> " + call(null, null));
        System.out.println("GET /api/orders       anonymous -> " + call(null, "ROLE_CUSTOMER"));
        System.out.println("GET /api/orders       asha      -> " + call(asha, "ROLE_CUSTOMER"));
        System.out.println("GET /api/admin/users  asha      -> " + call(asha, "ROLE_ADMIN"));
        System.out.println("GET /api/admin/users  meera     -> " + call(meera, "ROLE_ADMIN"));
    }
}

Output

GET /api/products     anonymous -> 200 OK
GET /api/orders       anonymous -> 401 Unauthorized (entry point: please log in)
GET /api/orders       asha      -> 200 OK
GET /api/admin/users  asha      -> 403 Forbidden    (access denied handler: asha lacks ROLE_ADMIN)
GET /api/admin/users  meera     -> 200 OK