🌐 Section 6 · Question #10

What is the difference between 401 and 403

The distinction between 401 Unauthorized and 403 Forbidden represents the fundamental architectural divide between two core security concepts: Authentication and Authorization.


🟢 Junior Level

The distinction between 401 Unauthorized and 403 Forbidden represents the fundamental architectural divide between two core security concepts: Authentication and Authorization.

Core Definitions

  • 401 Unauthorized (Authentication Failure) — “Who are you? I do not know you.”
    • The server cannot verify the identity of the client.
    • The request either completely lacks credentials (no Authorization header, no session cookie) or the provided credentials are invalid, malformed, or expired.
    • Re-authenticating (logging in, refreshing the JWT token) will resolve the issue.
  • 403 Forbidden (Authorization Failure) — “I know who you are, but you are not allowed in here.”
    • The client’s identity was successfully authenticated and verified, but the client lacks the required permissions, roles, or scopes to perform the requested operation on the target resource.
    • Re-authenticating with the same user credentials will not resolve the issue.

Real-World Analogy: Office Building Security

  • The 401 Scenario: You arrive at the turnstiles of a corporate office building, but you left your employee badge at home. The security guard stops you: “Please present valid identification to prove who you are.”
  • The 403 Scenario: You show your valid junior developer badge to the security guard (identity confirmed), but you attempt to unlock the high-security server room door. The guard stops you: “You are identified as a developer, but your security clearance does not permit entry to this room.”

Expected Client-Side Handling

  • Upon Receiving 401: A Single-Page Application (SPA) or mobile app intercepts the status, attempts a background silent token refresh using a Refresh Token, or redirects the user to /login.
  • Upon Receiving 403: The client displays an “Access Denied / Insufficient Permissions” banner without logging the user out.

🟡 Middle Level

The Historical Linguistic Defect in RFC 9110

A known linguistic flaw exists in the HTTP specification:

  • The status code is named 401 Unauthorized, but semantically it represents Unauthenticated (identity unverified).
  • True Authorization (permission checking) is governed by 403 Forbidden.

Protocol-Level Differences (RFC 9110)

Architectural Dimension 401 Unauthorized (RFC 9110 §15.5.2) 403 Forbidden (RFC 9110 §15.5.4)
Primary Root Cause Missing, invalid, or expired credentials Insufficient roles, scopes, IP block, or CORS rejection
Mandatory Header Must contain WWW-Authenticate WWW-Authenticate is not permitted
Repeat Request Policy Repeating the request with valid credentials may succeed Repeating the request with identical credentials will fail
OAuth 2.0 / JWT Lifecycle Token missing, corrupted, or expired (exp elapsed) Token is valid, but missing required scope or roles

Mandatory WWW-Authenticate Header on 401:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com", error="invalid_token", error_description="The access token expired"
Content-Type: application/problem+json

Spring Security 6.x Architecture: 401 vs. 403

In Spring Security, the generation of 401 and 403 status codes is strictly bifurcated into two core interfaces:

                          Incoming HTTP Request
                                    │
                       ┌────────────┴────────────┐
                       ▼                         ▼
             Unauthenticated User?        Authenticated User?
          (AuthenticationException)     (AccessDeniedException)
                       │                         │
                       ▼                         ▼
            AuthenticationEntryPoint        AccessDeniedHandler
                       │                         │
                       ▼                         ▼
             HTTP 401 Unauthorized      HTTP 403 Forbidden
  1. AuthenticationEntryPoint: Catches AuthenticationException when an unauthenticated anonymous user requests a protected endpoint, returning 401 Unauthorized.
  2. AccessDeniedHandler: Catches AccessDeniedException when an authenticated user attempts to execute a method guarded by @PreAuthorize("hasRole('ADMIN')") while holding only ROLE_USER, returning 403 Forbidden.
  3. CSRF Token Failures: In stateful Spring Security applications, a missing or invalid CSRF token generates a 403 Forbidden, not a 401!

🔴 Senior Level

Security vs. Information Disclosure (IDOR & Resource Enumeration)

At the senior engineering level, returning 403 Forbidden presents a serious security vulnerability known as Resource Enumeration (information disclosure):

Assume an attacker probes private tenant documents:

  • GET /api/v1/documents/999 $\to$ Returns 404 Not Found.
  • GET /api/v1/documents/1000 $\to$ Returns 403 Forbidden.

The 403 Forbidden status immediately confirms to the attacker that Document 1000 actually exists in the database, enabling targeted Insecure Direct Object Reference (IDOR) attacks or tenant discovery.

Senior Architectural Pattern: Masking with “Silent 404”

In multi-tenant SaaS applications, banking platforms, and classified APIs, if a user queries a resource belonging to another organization, the API returns 404 Not Found instead of 403 Forbidden. This ensures non-accessible resources appear non-existent.


Production-Ready Spring Security 6 Configuration with RFC 9457 Problem Details

package com.example.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.net.URI;
import java.time.Instant;

@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {

    private final ObjectMapper objectMapper;

    public SecurityConfiguration(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/public/*").permitAll()
                .requestMatchers("/api/v1/admin/*").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .exceptionHandling(ex -> ex
                // Handler for unauthenticated requests (401)
                .authenticationEntryPoint(customAuthenticationEntryPoint())
                // Handler for unauthorized/forbidden requests (403)
                .accessDeniedHandler(customAccessDeniedHandler())
            );

        return http.build();
    }

    @Bean
    public AuthenticationEntryPoint customAuthenticationEntryPoint() {
        return (request, response, authException) -> {
            ProblemDetail problem = ProblemDetail.forStatusAndDetail(
                    HttpStatus.UNAUTHORIZED, 
                    "Full authentication is required to access this resource"
            );
            problem.setTitle("Unauthorized");
            problem.setType(URI.create("https://api.example.com/errors/unauthorized"));
            problem.setProperty("timestamp", Instant.now());

            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
            response.setHeader("WWW-Authenticate", "Bearer realm=\"api.example.com\"");
            objectMapper.writeValue(response.getOutputStream(), problem);
        };
    }

    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            ProblemDetail problem = ProblemDetail.forStatusAndDetail(
                    HttpStatus.FORBIDDEN, 
                    "You do not possess the required permissions or roles to perform this action"
            );
            problem.setTitle("Forbidden");
            problem.setType(URI.create("https://api.example.com/errors/forbidden"));
            problem.setProperty("timestamp", Instant.now());

            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
            objectMapper.writeValue(response.getOutputStream(), problem);
        };
    }
}

OAuth 2.0 Scopes vs. Application Roles

In distributed microservices utilizing JWT tokens:

  1. Missing Role (403 Forbidden): The user authenticated as an operator (ROLE_OPERATOR), but the endpoint requires @PreAuthorize("hasRole('MANAGER')").
  2. Missing OAuth Scope (403 Forbidden with insufficient_scope): A third-party client authenticated with a token issued on behalf of a manager, but during the OAuth consent screen, the user only granted read permissions. Attempting to execute DELETE yields 403 Forbidden:
    HTTP/1.1 403 Forbidden
    WWW-Authenticate: Bearer error="insufficient_scope", scope="write:orders"
    
  3. Expired JWT (401 Unauthorized): The token’s exp claim has passed. The server must return 401, never 403, allowing the HTTP interceptor to trigger a silent refresh token exchange.

4 Tricky Questions

1. What status code must a server return if an incoming JWT token has expired?

Answer: The server must return 401 Unauthorized, accompanied by a header such as WWW-Authenticate: Bearer error="invalid_token", error_description="The token expired".

  • Returning 403 Forbidden is an architectural defect: modern web and mobile HTTP interceptors listen specifically for 401 to trigger automated, silent OAuth2 refresh token flows.
  • If the server returns 403, the client will assume access is permanently denied and show an error dialog rather than refreshing the token.

2. Why does RFC 9110 name status code 401 “Unauthorized” when it actually represents an authentication failure?

Answer: This is a historical linguistic misnomer originating in the early 1990s HTTP/1.0 specifications:

  • The authors named the status after the Authorization request header and the HTTP Basic Authentication scheme.
  • In modern security terminology, 401 represents an authentication failure (identity unknown or unverified), whereas true authorization (permission check) is represented by 403 Forbidden.

3. Under what conditions will an authenticated administrator receive a 403 Forbidden in Spring Security?

Answer: An administrator can receive a 403 Forbidden in several valid scenarios:

  1. CSRF Token Mismatch: In stateful session-based applications, submitting a form without a valid CSRF token triggers a 403 Forbidden.
  2. IP Whitelist / Firewall Restrictions: Spring Security rules configured with .hasIpAddress("10.0.0.0/24") will reject requests from external IPs with a 403.
  3. Missing OAuth 2.0 Scopes: If the administrator accesses the API through a third-party application whose token lacks the required OAuth scope.
  4. Tenant Isolation / ABAC Rules: In Attribute-Based Access Control, an admin of Tenant A attempting to access Tenant B’s data is denied with a 403 (or masked 404).

4. Why do multi-tenant SaaS architectures intentionally mask 403 Forbidden with 404 Not Found?

Answer: To prevent Information Disclosure and IDOR (Insecure Direct Object Reference) Enumeration Attacks:

  • If an API returns 403 Forbidden for IDs that exist but belong to other tenants, and 404 Not Found for non-existent IDs, an attacker can iterate through sequential IDs (/api/v1/invoices/1000, /1001) to map out which invoices exist.
  • Masking cross-tenant authorization failures with 404 Not Found ensures that unauthorized resources remain completely invisible and undetectable.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

  • 401 Unauthorized represents an Authentication failure: the client’s identity is unknown, unverified, or expired. Per RFC 9110, the response must include a WWW-Authenticate header. It triggers a login or refresh token flow.
  • 403 Forbidden represents an Authorization failure: the identity is successfully established, but the client lacks sufficient roles, permissions, or scopes. Re-authenticating with the same credentials will not help.
  • In Spring Security, 401 is handled by AuthenticationEntryPoint, and 403 is handled by AccessDeniedHandler.
  • In high-security multi-tenant SaaS systems, 403 is frequently masked as 404 Not Found to prevent IDOR resource enumeration.

Red Flags (What NOT to Say)

  • ❌ “401 and 403 are interchangeable synonyms.” (Fails fundamental distinction between authentication and authorization).
  • ❌ “If a JWT token expires, the server should return 403 Forbidden.” (Breaks automated client refresh token interception; expired tokens must return 401).
  • ❌ “To resolve a 403 Forbidden error, the user just needs to log in again.” (Logging in with the same account does not grant new privileges).
  • ❌ “A 401 response does not require any specific headers.” (RFC 9110 strictly mandates the WWW-Authenticate header).