DEV Community

Chanpenh Yorn
Chanpenh Yorn

Posted on Originally published at devpenh.com Fully Autonomous

401 Unauthorized with a valid JWT? Five causes I check first

"The token is fine, it works in Postman, but the API returns 401." A 401 with a JWT is one of two problems: the token never reached the verifier in the right shape, or it arrived and failed a check. These are the causes I check first. Every error message below was run on Node.js 24 with jsonwebtoken 9.0.2.

1. Read the response header first

A good API says why it rejected you:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="token expired"
Enter fullscreen mode Exit fullscreen mode

No error attribute usually means the server found no token at all. error="invalid_token" means it found one and verification failed.

2. The header is not exactly Bearer <token>

All of these fail inside the library with the same unhelpful message:

jwt.verify("Bearer " + token, secret);     // JsonWebTokenError: invalid token
jwt.verify(JSON.stringify(token), secret); // JsonWebTokenError: invalid token  (quotes from localStorage)
jwt.verify(token + "\n", secret);          // JsonWebTokenError: invalid token  (copied from a terminal)
jwt.verify("", secret);                    // JsonWebTokenError: jwt must be provided
Enter fullscreen mode Exit fullscreen mode

Bearer undefined on the client ends up as the last one.

3. The verifier and the token disagree

invalid signature                                    -> different secret/key, often another environment
jwt expired                                          -> exp is in the past
jwt audience invalid. expected: api://orders         -> token issued for another API
jwt issuer invalid. expected: https://auth.example.com -> staging token sent to production
invalid algorithm                                    -> RS256 token, HS256 verifier
Enter fullscreen mode Exit fullscreen mode

Decode the token (no secret needed) and compare alg, aud, iss and exp with your verify options.

4. It only fails in the browser

If curl works and the browser shows a CORS error, look for the OPTIONS preflight. Browsers never attach the token to it, so an auth middleware that runs first answers 401 and the browser reports a CORS failure. Put CORS before auth:

app.use(cors({ origin: "http://localhost:5173" }));
app.use(requireAuth);
Enter fullscreen mode Exit fullscreen mode

5. A redirect dropped the header

fetch (browsers and Node) removes Authorization when a redirect goes to another origin, so an http to https or a host redirect can silently strip it.

One trap that does not give a 401: an exp set with Date.now() (milliseconds) passes verification, because the token now expires tens of thousands of years from now.

The full checklist, with a tested Express middleware that returns a useful WWW-Authenticate header, is here: https://devpenh.com/guides/debug-401-unauthorized-jwt/

Disclosure: I run that site. Its JWT decoder (https://devpenh.com/tools/jwt-decoder/) decodes tokens in the browser, so you can inspect a real token from a failing request without sending it anywhere.

This post was written with AI assistance; the code and error messages were run and checked before publishing.

Top comments (0)