SiteError.comYour friendly guide to HTTP status codes
Status CodesBlog
  1. Home
  2. Blog
  3. Understanding HTTP 401 Unauthorized: Authentication, WWW-Authenticate, and the 401 vs 403 Trap

Understanding HTTP 401 Unauthorized: Authentication, WWW-Authenticate, and the 401 vs 403 Trap

May 20, 20269 min read
4xxClient Error

401 Unauthorized has a misleading name. Read it literally and you'd think it means "you're not allowed" — but that's actually 403's job. 401 is about authentication: proving who you are. The word "unauthorized" in the spec is a historical accident, and untangling it from "authorization" is the single most useful thing you can learn about this code. Let's go through what 401 really means, the header it's required to send, when to reach for it instead of 403 or 407, and how to return and handle it without shooting yourself in the foot.

What Is a 401?

A 401 means the server couldn't apply your request because you haven't proven your identity — you sent no credentials, or the ones you sent are invalid or expired.

The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. — RFC 9110, Section 15.5.2

In plain English: "I don't know who you are, and I need to before I can do this." The fix for a 401 is almost always authenticate and try again — log in, refresh your token, send the right header.

401 Is About Authentication, Not Authorization

This is the trap, so it's worth stating bluntly. There are two distinct questions a server asks about a request:

  • Authentication — Who are you? Can you prove your identity? (401's domain.)
  • Authorization — Are you allowed? Does your proven identity have permission? (403's domain.)

A 401 says you failed the first question. You never got far enough for the second one to matter. The mental model that keeps it straight:

  • 401 = "Who are you?" — You haven't proven it. Authenticating again can fix this.
  • 403 = "I know who you are, and no." — You're authenticated fine; you just don't have permission. Authenticating again cannot fix this — you'd need a different role, plan, or grant.

If logging in or sending a fresh token could plausibly resolve the situation, it's a 401. If the user is already who they say they are and still can't proceed, it's a 403.

The WWW-Authenticate Header Is Mandatory

This is the part most implementations get wrong. A 401 isn't just a status number — the spec requires you to tell the client how to authenticate:

The server generating a 401 response MUST send a WWW-Authenticate header field containing at least one challenge applicable to the target resource. — RFC 9110, Section 15.5.2

The WWW-Authenticate header carries a challenge — the scheme the client should use:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

Common schemes:

  • Bearer — token auth (OAuth 2.0, JWT). By far the most common in modern APIs. You can add detail: WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired".
  • Basic — username/password, base64-encoded. Triggers the browser's native login dialog: WWW-Authenticate: Basic realm="Admin Area".
  • Digest — a hashed-credential scheme, rarely used in new systems.

Omitting WWW-Authenticate is the most common 401 spec violation in the wild. It's tempting to skip — APIs return a bare 401 and move on — but it leaves clients (and browsers) with no machine-readable signal about how to recover. At minimum, send the scheme.

401 vs 403 vs 407

Three codes deal with "you can't have this yet," and they're routinely mixed up. Choose deliberately:

CodeMeaningUse when
401 UnauthorizedNot authenticatedNo credentials, or they're invalid/expired
403 ForbiddenAuthenticated but not allowedValid identity, but lacks permission (wrong role, plan, region)
407 Proxy Authentication RequiredMust authenticate to a proxyAn intermediary needs auth, not the origin server

407 is the odd one out: it's structurally identical to 401 but the challenge comes from a proxy, so it uses Proxy-Authenticate instead of WWW-Authenticate. You'll mostly see it on corporate networks.

The heuristic again: 401 = "Who are you?", 403 = "I know who you are, and no.", 407 = "The proxy in the middle needs to know who you are."

Common Causes

In practice, 401s come from a short list of repeat offenders:

  1. Missing token — No Authorization header at all. The client forgot to attach it, or a logged-out user hit a protected route.
  2. Expired session or token — The JWT's exp passed, or the server-side session timed out. The credentials were valid; they aren't anymore.
  3. Invalid credentials — Wrong password, malformed token, a token signed with a key the server doesn't trust.
  4. Clock skew — A subtle one: if the client and server clocks disagree by more than the allowed leeway, a perfectly fresh JWT can read as expired (or not-yet-valid via nbf). Sync clocks with NTP and allow a few seconds of leeway when validating.
  5. Malformed Authorization header — Sending Authorization: <token> instead of Authorization: Bearer <token>. The scheme prefix is part of the format.
  6. Revoked or rotated token — An OAuth access token that was revoked, or signed with a key that has since rotated out of the JWKS.

Returning 401 Correctly Across Platforms

Express / Node.js

function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) {
    res.set("WWW-Authenticate", 'Bearer realm="api"');
    return res.status(401).json({ error: "missing_token" });
  }
  try {
    req.user = verifyJwt(token);
    next();
  } catch {
    res.set(
      "WWW-Authenticate",
      'Bearer error="invalid_token", error_description="Token is invalid or expired"',
    );
    return res.status(401).json({ error: "invalid_token" });
  }
}

Note the WWW-Authenticate header on both branches — missing and invalid. That's what the spec asks for.

Next.js App Router

In a route handler, return a NextResponse with the status and challenge header:

// app/api/me/route.ts
import { NextResponse } from "next/server";
 
export async function GET(request: Request) {
  const token = request.headers.get("authorization")?.split(" ")[1];
  if (!token || !(await isValid(token))) {
    return NextResponse.json(
      { error: "invalid_token" },
      {
        status: 401,
        headers: { "WWW-Authenticate": 'Bearer realm="api"' },
      },
    );
  }
  return NextResponse.json({ user: await getUser(token) });
}

For page-level protection, prefer redirecting unauthenticated users to a login route in middleware — a 401 on an HTML navigation is awkward UX. Reserve raw 401s for API endpoints and fetch calls.

NGINX

location /admin/ {
  auth_basic "Admin Area";
  auth_basic_user_file /etc/nginx/.htpasswd;
}

auth_basic makes NGINX send 401 with WWW-Authenticate: Basic realm="Admin Area" automatically, triggering the browser's login prompt. To return a 401 by hand (e.g., from an auth_request subrequest), set the header explicitly with add_header WWW-Authenticate ... always;.

REST API JSON body

For machine consumers, pair the header with a structured body so clients can branch on a stable code:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Content-Type: application/json
 
{
  "error": "invalid_token",
  "message": "The access token has expired.",
  "documentation": "https://api.example.com/docs/errors#invalid_token"
}

Don't return 200 OK with { "error": "unauthorized" }. Any decent HTTP client treats 2xx as success, your token-refresh logic will never trigger, and you'll special-case every call site forever.

Handling 401 on the Client

The classic pattern is the silent refresh-and-retry: a 401 means "your access token is stale," so refresh it once and replay the request.

async function fetchWithAuth(input, init = {}) {
  let res = await fetch(input, withToken(init, getAccessToken()));
 
  if (res.status === 401) {
    const refreshed = await refreshAccessToken(); // uses the refresh token
    if (!refreshed) {
      redirectToLogin();
      return res;
    }
    res = await fetch(input, withToken(init, getAccessToken())); // retry once
  }
 
  return res;
}

Two things to get right:

  • Retry exactly once. If the request still 401s after a refresh, stop and send the user to log in. Looping on refresh is how you get an infinite request storm against your auth server.
  • Don't treat 403 like 401. A 403 means refreshing won't help — the user is authenticated but lacks permission. Refreshing and retrying a 403 just wastes a round trip and hides the real problem from the user. Branch on the status code.

401 and SEO

Search crawlers can't log in. When Googlebot hits a 401, it sees a locked door and moves on — the content behind it never gets indexed.

  • Don't gate SEO-important content behind 401. If a page needs to rank, it has to be reachable without credentials.
  • Offer a public preview. A common pattern for paywalled or member content is to serve a meaningful summary (indexable, 200 OK) and put only the full content behind auth.
  • 401 is a soft signal. Crawlers may re-check a 401 URL periodically in case access rules change, but they won't index what they can't read.

Common Pitfalls

  1. Using 403 where 401 belongs (and vice versa). Returning 403 to a logged-out user is wrong — they could fix it by logging in, so it's a 401. Returning 401 to a logged-in user who lacks permission is also wrong — re-authenticating won't help, so it's a 403.
  2. Omitting WWW-Authenticate. A bare 401 with no challenge violates the spec and leaves clients guessing how to recover.
  3. Returning 200 with an error body. Breaks every client's success/failure branching and silently disables token-refresh flows.
  4. Leaking resource existence via inconsistent codes. If /users/42 returns 403 (exists, no access) but /users/99 returns 404 (doesn't exist), an attacker can enumerate which IDs are real. For sensitive resources, return the same code regardless — usually 404 to deny existence entirely.
  5. 401 → login redirect loops. If your login page itself sits behind the auth check, an unauthenticated user gets redirected forever. Exempt the auth routes.
  6. Confusing an expired token with an invalid one. Both are 401, but distinguishing them in WWW-Authenticate (error="invalid_token" with a description) lets clients decide whether to refresh or force a full re-login.

Wrapping Up

401 is small but routinely misused. The rules of thumb:

  • 401 is about authentication (who you are), not authorization (what you're allowed) — that's 403
  • Always send a WWW-Authenticate challenge; it's required, not optional
  • Use 401 when credentials are missing/invalid/expired, 403 when the user is authenticated but lacks permission, 407 when a proxy needs auth
  • On the client, refresh-and-retry once, then redirect to login — and never retry a 403
  • Never return 200 with an error body, and keep 401/403/404 consistent so you don't leak which resources exist

For more, see our pages on 401 Unauthorized, 403 Forbidden, and 407 Proxy Authentication Required. The line between "doesn't exist" and "you can't see it" comes up again in our understanding 404 Not Found post, and you can compare 401 vs 403 side by side.

Related Status Codes

🤨400Bad Request🔐401Unauthorized💳402Payment Required🚫403Forbidden🔍404Not Found🙅405Method Not Allowed🍽️406Not Acceptable🎫407Proxy Authentication Required⏰408Request Timeout⚔️409Conflict👻410Gone📏411Length Required❌412Precondition Failed📦413Payload Too Large📜414URI Too Long📼415Unsupported Media Type📖416Range Not Satisfiable😞417Expectation Failed🫖418I'm a Teapot🚪421Misdirected Request🤔422Unprocessable Entity🔒423Locked🎯424Failed Dependency⏰425Too Early⬆️426Upgrade Required🔑428Precondition Required🚦429Too Many Requests📋431Request Header Fields Too Large⚖️451Unavailable For Legal Reasons
Back to Blog

Popular Status Codes

  • 200 OK
  • 301 Moved Permanently
  • 302 Found
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable

Compare Codes

  • 401 vs 403
  • 301 vs 302
  • 404 vs 410
  • 500 vs 502
  • Compare any codes →

Categories

  • Informational
  • Success
  • Redirection
  • Client Error
  • Server Error
  • NGINX
  • Cloudflare
  • AWS ELB
  • Microsoft IIS

Tools

  • Cheat Sheet
  • Status Code Quiz
  • URL Checker
  • API Playground
  • Blog

© 2026 SiteError.com. All rights reserved.