Careeroza
401 vs 403
Two Different Questions
| Status | Meaning | Question that failed | ShopEase example | Client should |
|---|---|---|---|---|
| 401 Unauthorized | Not authenticated | "Who are you?" | No token, expired token, bad signature, wrong password | Refresh the token or show login |
| 403 Forbidden | Authenticated, but not allowed | "May you do this?" | Asha calls /api/admin/users; missing CSRF token | Show "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 -> 403Example
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