Definition
- 401 Unauthorized – the client is not authenticated; no valid credentials were provided; the response should include a
WWW-Authenticateheader. - 403 Forbidden – the client is authenticated but does not have permission; repeating with the same credentials will not help.
- Order of checks – first authenticate (who are you?), then authorise (are you allowed?); never return 403 to an unauthenticated client.
- Retryability – 401 is retryable after obtaining fresh credentials; 403 is not retryable with the same identity.
- Security nuance – sometimes return 404 instead of 403 to avoid leaking resource existence to unauthorised users.
Imagine the National Bank of India branch. At the front door, a guard asks for your ID. If Rajesh has no ID card, the guard refuses entry and says "come back with identification": that is 401, we do not know who you are. Now suppose Rajesh enters with a valid ID, but he walks toward the vault room reserved for managers. The guard recognises him perfectly but says "you are not allowed in here": that is 403, we know exactly who you are, but you lack permission.
Understanding the Concepts
What is 401 Unauthorized?
401 Unauthorized is returned when authentication is missing or invalid. The request carried no credentials, or the token was expired or malformed. The correct client response is to authenticate and retry. By specification, a 401 response should include a WWW-Authenticate header describing how to authenticate.
package com.bank.security;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
public class AuthExceptionHandler {
// 401: no or invalid token → client must authenticate.
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiError> handleAuth(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.header("WWW-Authenticate", "Bearer")
.body(new ApiError("AUTH_REQUIRED", "Authentication required"));
}
}
What is 403 Forbidden?
403 Forbidden is returned when the client is properly authenticated, the server knows who they are, but their identity lacks the permission or role needed for the action. Retrying with the same token will not help. The problem is not identity but authorisation.
// 403: authenticated but lacks the required role.
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiError> handleForbidden(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ApiError("FORBIDDEN",
"You do not have permission for this resource"));
}
How do 401 and 403 differ? (Comparison table)
| Aspect | 401 Unauthorized | 403 Forbidden |
|---|---|---|
| Meaning | Not authenticated | Authenticated but not permitted |
| Cause | Missing, expired, or invalid credentials | Valid credentials but insufficient role/permission |
| Client action | Authenticate (log in, refresh token) and retry | Do not retry with same identity; request elevated access |
| Response header | Should include WWW-Authenticate | No special header |
| Retryable | Yes, after obtaining fresh credentials | No, not with same identity |
| Example | Expired JWT token | Customer accessing admin-only report |
// 401 scenario: Rajesh's JWT token expired.
// GET /api/v1/accounts/SAV2001
// Authorization: Bearer <expired-token>
// → 401 Unauthorized + WWW-Authenticate: Bearer
// Client refreshes token and retries.
// 403 scenario: Rajesh is a customer, not an admin.
// GET /api/v1/admin/fraud-report
// Authorization: Bearer <valid-customer-token>
// → 403 Forbidden
// Retrying with the same token will not help.
What is the correct order of checks?
The order matters. First authenticate, which answers "who are you?" If that fails, return 401. Only after successful authentication do you check authorisation, which answers "are you allowed?" If that fails, return 403. You never return 403 to an unauthenticated client, because you cannot judge permission without knowing identity.
package com.bank.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
public class SecurityFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// Step 1: Authenticate – who are you?
String token = req.getHeader("Authorization");
if (token == null || !tokenService.isValid(token)) {
res.setStatus(401); // Not authenticated
res.setHeader("WWW-Authenticate", "Bearer");
return;
}
// Step 2: Authorise – are you allowed?
User user = tokenService.extractUser(token);
if (!user.hasPermission(req.getRequestURI())) {
res.setStatus(403); // Authenticated but not permitted
return;
}
chain.doFilter(request, response);
}
}
When should you return 404 instead of 403?
A security nuance: sometimes you deliberately return 404 instead of 403 to avoid leaking resource existence. If Rajesh asks for Priya's private account and is authenticated but not authorised, a 403 tells him the account exists but he cannot see it. For sensitive resources, returning 404 hides whether the resource exists at all, trading precise semantics for privacy.
// Standard: 403 reveals the resource exists.
// GET /api/v1/accounts/SAV5001 (Priya's private account)
// Rajesh is authenticated but not the owner.
// → 403 Forbidden ← Rajesh now knows SAV5001 exists
// Secure: 404 hides existence.
// → 404 Not Found ← Rajesh cannot tell if it exists
// This is a deliberate, documented trade-off.
How does Spring Security handle 401 vs 403?
In Spring Security, an AuthenticationEntryPoint handles authentication failures and returns 401, while an AccessDeniedHandler handles authorisation failures and returns 403. Method-level security annotations like @PreAuthorize with role expressions trigger a 403 when the authenticated user lacks the role.
package com.bank.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> {
res.setStatus(401); // Not authenticated
res.setHeader("WWW-Authenticate", "Bearer");
})
.accessDeniedHandler((req, res, e) -> {
res.setStatus(403); // Authenticated but not allowed
})
);
return http.build();
}
}
What are common mistakes with 401 and 403?
Confusing the two is a common and costly mistake. Returning 403 when the token simply expired makes clients give up instead of refreshing. Returning 401 when a user genuinely lacks a role makes clients loop forever trying to re-authenticate, never realising the problem is permission.
// Wrong: 403 for expired token – client gives up instead of refreshing.
// Rajesh's token expired.
// → 403 Forbidden ← Client thinks permission problem, stops retrying
// Correct: 401 – client refreshes token and retries.
// Wrong: 401 for insufficient role – client loops refreshing forever.
// Rajesh has a valid token but is not an admin.
// → 401 Unauthorized ← Client refreshes token, retries, gets 401 again, loops forever
// Correct: 403 – client shows "you do not have permission."
What should a senior engineer know about 401 vs 403?
A senior engineer knows that 401 triggers automatic token refresh in well-built clients, while 403 surfaces a permission error to the user or triggers an elevated-access flow. They know the security trade-off of returning 404 instead of 403 for sensitive resources. They know that in Spring Security, AuthenticationEntryPoint handles 401 and AccessDeniedHandler handles 403, and they configure both explicitly rather than relying on defaults.
Visual Overview
┌───────────────────────────────────────────────────────┐ │ 401 vs 403 Decision Flow │ ├───────────────────────────────────────────────────────┤ │ │ │ Incoming request │ │ │ │ │ ▼ │ │ Step 1: Authenticate – who are you? │ │ │ │ │ ┌────┴────┐ │ │ │ Valid │ No credentials / expired / malformed │ │ │ creds? │──────────────▶ 401 Unauthorized │ │ └────┬────┘ + WWW-Authenticate: Bearer │ │ │ Yes │ │ ▼ │ │ Step 2: Authorise – are you allowed? │ │ │ │ │ ┌────┴────┐ │ │ │ Has │ Lacks required role or permission │ │ │ perm? │──────────────▶ 403 Forbidden │ │ └────┬────┘ (retrying won't help) │ │ │ Yes │ │ ▼ │ │ Process request → 200 OK │ │ │ │ Security variant: │ │ For sensitive resources, return 404 instead │ │ of 403 to hide resource existence. │ └───────────────────────────────────────────────────────┘
Key Takeaway
HTTP 401 and 403 both refuse a request, but they refuse it for different reasons, and the difference is identity versus permission. A 401 Unauthorized means the client is not authenticated: no valid credentials were provided, the token is missing or expired, and the correct client response is to authenticate and retry. A 403 Forbidden means the client is authenticated, the server knows exactly who they are, but their identity lacks the permission for this action, and retrying with the same credentials will not help.
The order of checks captures the idea cleanly. First the server authenticates, which answers "who are you?" If that fails, it returns 401. Only after successful authentication does it authorise, which answers "are you allowed?" If that fails, it returns 403. The server never returns 403 to an unauthenticated client because it cannot judge permission without first knowing identity.
At National Bank of India this distinction fixed two opposite bugs. When Rajesh's session token expired, the old API returned 403, so the app treated it as a permanent permission failure and logged him out entirely, when all it needed was a token refresh. The correct code is 401, which lets the app refresh the token and retry smoothly. The opposite case was Rajesh hitting an admin-only fraud report with a valid customer token. His token was fine, but his role was CUSTOMER, so the correct code is 403, and returning 401 would have made the app loop forever trying to re-authenticate.
The security nuance is that for sensitive resources, 404 can replace 403 to avoid leaking that the resource exists to someone not allowed to see it. In Spring Security, AuthenticationEntryPoint returns 401 and AccessDeniedHandler returns 403. Getting the pair right keeps client behaviour correct and the security audit trail clean.
Remember one sentence: 401 means "I do not know who you are, please authenticate," while 403 means "I know exactly who you are, and you are not allowed."
---