1. Library
  2. Computer Networks
  3. Http and the Web
  4. Status Codes

Updated 9 hours ago

The HTTP specification made an unfortunate naming choice. 401 is called "Unauthorized" but actually means unauthenticated—the server doesn't know who you are. 403 "Forbidden" is the real authorization failure—the server knows exactly who you are and has decided you can't do that.

This naming confusion causes more API bugs than almost any other HTTP misunderstanding. Once you see the distinction clearly, you'll never mix them up again.

The Building Security Metaphor

This isn't just an analogy—it's actually how authentication and authorization work:

401 Unauthorized: You're at the door without an ID badge. Security doesn't know who you are. They can't check your clearance level because they don't know your identity. Show your badge first.

403 Forbidden: You have your ID badge. Security knows you're Alice from Engineering. But you're trying to enter the Executive Floor, and Alice from Engineering isn't on the access list. No amount of re-showing your badge will change this.

The guard always checks identity before checking permissions. Authentication precedes authorization. Always.

401: "Who Are You?"

A 401 response means:

  • No credentials were provided, OR
  • The credentials are invalid (wrong password, malformed token), OR
  • The credentials have expired

The solution is always the same: provide valid credentials and try again.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"

{
    "error": "Authentication required"
}

The WWW-Authenticate header is required by the HTTP specification. It tells the client what authentication method to use—Bearer tokens, Basic auth, or something else. Omitting this header is a spec violation.

When to Return 401

No credentials provided:

GET /api/profile HTTP/1.1
// No Authorization header

→ 401 Unauthorized

Invalid credentials:

POST /api/login HTTP/1.1
{"username": "alice", "password": "wrong"}

→ 401 Unauthorized

Expired token:

GET /api/profile HTTP/1.1
Authorization: Bearer expired-token

→ 401 Unauthorized

How Clients Should Respond

When you receive 401: re-authenticate. Refresh the token. Prompt the user to log in again. Then retry the request.

async function makeRequest(url) {
    const response = await fetch(url, {
        headers: { 'Authorization': `Bearer ${getToken()}` }
    });

    if (response.status === 401) {
        const newToken = await refreshToken();
        if (newToken) {
            return fetch(url, {
                headers: { 'Authorization': `Bearer ${newToken}` }
            });
        }
        redirectToLogin();
    }

    return response;
}

403: "The Answer Is No"

A 403 response means:

  • The server knows who you are (authentication succeeded)
  • You don't have permission to do this specific thing
  • Re-authenticating won't help—this is a policy decision
HTTP/1.1 403 Forbidden

{
    "error": "You don't have permission to access this resource"
}

No WWW-Authenticate header needed. The problem isn't authentication.

When to Return 403

Accessing another user's data:

DELETE /api/users/456 HTTP/1.1
Authorization: Bearer valid-token-for-user-123

→ 403 Forbidden (you can only delete your own account)

Missing required role:

POST /api/admin/users HTTP/1.1
Authorization: Bearer valid-token-for-regular-user

→ 403 Forbidden (admin role required)

Account suspended:

GET /api/data HTTP/1.1
Authorization: Bearer valid-token-for-suspended-user

→ 403 Forbidden (account suspended)

How Clients Should Respond

When you receive 403: don't retry. The problem isn't your credentials—it's your permissions. Show the user an error message. Navigate them somewhere they can actually go.

if (response.status === 403) {
    showError("You don't have permission to do this");
    redirectToHome();
    // Do NOT retry. It won't help.
}

The Decision

When a request comes in:

  1. Are credentials present? No → 401
  2. Are credentials valid? No → 401
  3. Does this user have permission? No → 403
  4. Otherwise → Process the request

Authentication happens before authorization. You can't check permissions for someone you haven't identified.

Complete Example

An admin-only endpoint:

// No credentials
POST /api/admin/users HTTP/1.1
→ 401 Unauthorized

// Valid credentials, regular user
POST /api/admin/users HTTP/1.1
Authorization: Bearer token-for-alice
→ 403 Forbidden

// Valid credentials, admin user  
POST /api/admin/users HTTP/1.1
Authorization: Bearer token-for-admin
→ 201 Created

Server Implementation

function requireAuth(request, response, next) {
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
        return response.status(401)
            .set('WWW-Authenticate', 'Bearer realm="api"')
            .json({ error: 'Authentication required' });
    }

    try {
        request.user = verifyToken(token);
        next();
    } catch (error) {
        return response.status(401)
            .set('WWW-Authenticate', 'Bearer realm="api"')
            .json({ error: 'Invalid token' });
    }
}

function requireAdmin(request, response, next) {
    // This middleware assumes requireAuth already ran
    if (!request.user.isAdmin) {
        return response.status(403)
            .json({ error: 'Admin role required' });
    }
    next();
}

// Usage: authentication first, then authorization
app.post('/api/admin/users', requireAuth, requireAdmin, handleCreateUser);

Common Mistakes

Using 401 when you mean 403:

DELETE /api/users/456
Authorization: Bearer valid-token-for-user-123

❌ 401 Unauthorized  // Wrong—they ARE authenticated
✓ 403 Forbidden

Using 403 when you mean 401:

GET /api/profile
// No Authorization header

❌ 403 Forbidden  // Wrong—you don't know who they are yet
✓ 401 Unauthorized

Forgetting WWW-Authenticate:

❌ HTTP/1.1 401 Unauthorized
   {"error": "Not authenticated"}

✓ HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="api"
   {"error": "Authentication required"}

Security Consideration: Hiding Resource Existence

Sometimes you want to hide whether a resource exists:

GET /api/documents/secret-id HTTP/1.1
Authorization: Bearer valid-token

// Option 1: Honest
→ 404 Not Found (reveals the resource doesn't exist)

// Option 2: Opaque  
→ 403 Forbidden (hides whether it exists)

Using 403 for nonexistent resources prevents enumeration attacks but reduces API transparency. Choose based on your security requirements.

Frequently Asked Questions About 401 and 403

Was this page helpful?

😔
🤨
😃
401 Unauthorized vs. 403 Forbidden • Library • Connected