401 vs 403: Pick the Right Status for a Refused Request

Return 401 Unauthorized when the request has no valid credentials: no token, an expired session, a bad signature. Logging in could fix it, and the response must carry a WWW-Authenticate header. Return 403 Forbidden when you know who the caller is and they still may not do this, so signing in again changes nothing.

Unauthorized versus Forbidden
401 Unauthorized403 Forbidden
Really meansNot authenticatedNot permitted
Will logging in help?YesNo
Required headerWWW-AuthenticateNone required
Typical causeMissing or expired token or sessionRole or ownership check failed
Client shouldSign in or refresh the token, then retryStop and show an access message

Updated

Answer four questions about the request your server is refusing. The picker returns the status to send, the header that goes with it, and a one-line reason you can put in a code comment. It also covers the case where you would rather not admit the resource exists.

Which status should I return?

Did the request carry credentials (session cookie, token, Authorization header)? Yes

Were those credentials valid and unexpired? Yes

Is this caller allowed to perform this action on this resource? No

Should callers without access be kept from learning the resource exists? No

Send this

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error": "forbidden"}

The caller is known and lacks permission. Signing in again will not help.

The check in code

Express middleware: 401 first, then 403

verifyToken stands in for your own session or JWT check. The order matters: authenticate, then authorise.

function requireRole(role) {
  return async (req, res, next) => {
    const header = req.get("Authorization") || "";
    const token = header.startsWith("Bearer ") ? header.slice(7) : null;
    if (!token) {
      res.set("WWW-Authenticate", 'Bearer realm="api"');
      return res.status(401).json({ error: "authentication_required" });
    }
    const user = await verifyToken(token).catch(() => null);
    if (!user) {
      res.set("WWW-Authenticate", 'Bearer realm="api", error="invalid_token"');
      return res.status(401).json({ error: "invalid_token" });
    }
    if (!user.roles.includes(role)) {
      return res.status(403).json({ error: "forbidden" });
    }
    req.user = user;
    next();
  };
}

Front end: react differently to each

One code path sends the user to sign in; the other explains the refusal.

const res = await fetch("/api/reports", { credentials: "include" });

if (res.status === 401) {
  window.location.assign("/login?next=/reports");
} else if (res.status === 403) {
  showMessage("Your account does not have access to reports.");
} else {
  render(await res.json());
}

Two different questions

Every protected request answers two questions in order. Who is calling? That is authentication, and failing it gives 401. Is this caller allowed to do this? That is authorisation, and failing it gives 403.

Keeping them apart helps clients. A 401 tells a front end to send the user to sign in. A 403 tells it that signing in again will not help and an access message belongs on screen instead.

What the specification adds

When a request already carried credentials and still gets a 401, the server is saying those credentials were refused. For a 403 the server may explain the reason in the body, or say nothing about it at all.

How it works

  1. Say whether the request arrived with any credentials at all.
  2. Say whether those credentials checked out.
  3. Say whether this caller has permission for this action on this resource.
  4. Decide whether the resource's existence should stay hidden from callers without access.

Tips

Frequently asked questions

What is the main difference between 401 and 403?

401 says the server does not know who you are, or did not accept the proof you sent. 403 says it knows who you are and the answer is still no.

Must a 401 include the WWW-Authenticate header?

Yes. RFC 9110 says a server generating a 401 response must send at least one WWW-Authenticate header field with a challenge, such as Bearer for token APIs.

Should an expired token get 401 or 403?

401. The credentials are no longer valid, and fresh ones would fix it. A Bearer challenge can say so with error="invalid_token".

Can I return 404 instead of 403?

Yes. The specification lets a server send 404 instead of 403 to hide whether the resource exists. It is a common choice for ids that belong to other users.

Does a 403 mean my IP address is blocked?

It can. Servers send 403 for any refusal they consider final for this request: IP rules, missing file permissions, a firewall rule or a failed role check. The response body or server log usually says which.

Related: HTTP status codes reference, Authentication guide, What is a JWT, OAuth explained, HTTP 405 Method Not Allowed.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap