← Blog
July 10, 2026 · 6 min

401 vs 403 in Token-Based Auth

401 means authenticate, 403 means you're authenticated but not allowed. Getting this distinction right in JWT-based APIs makes debugging far easier.

The distinction, precisely - **401 Unauthorized** means the request lacks valid authentication — the server doesn't know who you are, or doesn't trust your claim of who you are. Despite the name, it's really about **authentication**, not authorization. - **403 Forbidden** means the server knows exactly who you are, and the answer is still no — you're **authenticated but not authorized** for this particular action or resource.

Mixing these up doesn't just annoy API consumers, it actively misleads them about what to do next: retrying with a fresh token fixes a 401, but does nothing for a 403.

Mapping JWT failures to status codes - Missing `Authorization` header entirely → **401** - Malformed token (not valid JWT structure) → **401** - Signature verification fails → **401** - Token expired (`exp` in the past) → **401** - Token `aud` doesn't match this API → **401** (the token isn't valid *for you*, which is an authentication-layer problem) - Token is valid, but the user's role/scope doesn't permit this action → **403** - Token is valid, but the requested resource belongs to a different user/tenant → **403**

The general rule: if the problem can be fixed by presenting a different or refreshed token, it's a 401. If a different token from the same user wouldn't help — the user genuinely isn't allowed to do this — it's a 403.

A subtlety: resource existence and 403 vs 404 Some APIs intentionally return 404 instead of 403 for resources a user isn't allowed to see, to avoid leaking that the resource exists at all (common for private repos, documents, etc.). This is a legitimate design choice but is a deliberate exception, not a substitute for correct 401/403 usage elsewhere.

Client-side handling patterns - On **401**, attempt a silent token refresh once; if that also fails, redirect to login. - On **403**, do not attempt to refresh or retry automatically — show the user a clear "you don't have permission" message, since retrying with the same credentials will always fail the same way. - Avoid silently swallowing either status code — both are meaningful signals for debugging and for user-facing messaging.

A common implementation mistake A frequent bug in JWT middleware is returning 403 for every failure that occurs in the auth layer, including expired or malformed tokens, because it's the more "restrictive-sounding" code. This breaks client refresh logic, since clients typically only attempt a token refresh on 401. Keep the layers cleanly separated: your authentication middleware (verifying the token itself) should only ever emit 401; your authorization logic (checking roles, ownership, scopes) after that point emits 403.

Example middleware structure - **Authentication middleware**: parses the `Authorization` header, verifies signature, checks `exp`/`nbf`/`aud`/`iss`. Any failure here → 401. - **Authorization middleware/guard**: runs after authentication succeeds, checks the user's role or scope against the requested action. Any failure here → 403.

Checklist - Return 401 for anything wrong with the token itself: missing, malformed, expired, bad signature, wrong audience. - Return 403 only once you've established identity and the answer is still "not allowed." - Keep authentication and authorization as clearly separate middleware layers. - Make sure your client only attempts token refresh on 401, never on 403. - Document your API's 401/403 semantics explicitly — this is a frequent source of integration bugs for API consumers.