Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

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

"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"

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

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

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);

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.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.