JWT Token Expired, Invalid Signature, and Other JWT Errors Explained
A working JWT looks like gibberish; a broken one looks identical. Here is how to decode JWTs safely, debug the four most common errors, and read the iat / exp / nbf claims without guessing.
A JSON Web Token looks like three blobs of random characters separated by dots: eyJhbGciOi....eyJzdWIiOi....SflKxw.... When it works, your API returns 200. When it doesn't, you get a 401 with a message like "jwt expired", "invalid signature", or — worst of all — "jwt malformed", with no indication of which part is malformed.
This guide explains what's actually inside a JWT, how to decode one safely, and how to debug the four errors that account for ~95% of JWT problems in production.
What a JWT actually is (in 60 seconds)
A JWT is three base64url-encoded JSON objects joined by dots:
- Header — describes the signing algorithm (
alg) and token type (typ). - Payload — the actual claims: who the user is, when the token was issued, when it expires.
- Signature — a cryptographic signature over the first two parts, computed with a secret key only the server knows.
The payload is not encrypted. Anyone who has the token can decode and read it. The signature only proves that the payload hasn't been tampered with after the server signed it.
To see what's inside any JWT, paste it into our JWT decoder. It runs entirely in your browser — the token never leaves your device, which matters because JWTs often contain user IDs, email addresses, and session metadata.
The standard claims you should know
The JWT spec (RFC 7519) defines a handful of registered claim names. Three matter for almost every debugging session:
iat(issued at) — Unix timestamp when the token was created.exp(expiration) — Unix timestamp when the token stops being valid.nbf(not before) — Unix timestamp before which the token must be rejected. Often equal toiat, sometimes set to a future time for delayed activation.
These are seconds since 1970-01-01 UTC, not milliseconds. A 10-digit number means seconds (correct); a 13-digit number means your token issuer has a bug and is using JS-style milliseconds. Servers will reject the token because exp looks like the year 50,000.
Two more you'll see often: sub (subject — the user ID) and iss (issuer — which auth server minted the token).
Error 1 — "jwt expired"
The most common JWT error. The current time is past the exp claim. The token is dead and your client needs to either refresh or re-authenticate.
Decode the token and look at the exp claim. Convert it to a human-readable date — every JWT decoder does this automatically. Compare to the current time. If exp is in the past, your refresh-token logic isn't running, or the access token is short-lived (5–15 minutes is common) and the user has been idle.
A subtle variant: clock skew. If your client and server clocks differ by more than the allowed skew (often 60 seconds), a token issued at 12:00:00 with a 60-second lifetime can look "already expired" to a server whose clock is 90 seconds ahead. The fix is to sync clocks via NTP, or to widen the server's skew tolerance.
Error 2 — "invalid signature"
The signature doesn't match what the server computes from the header and payload using its secret key. This means one of three things:
- The token was tampered with. Someone modified the payload (common in test/debug attempts) and forgot that changing the payload invalidates the signature.
- You're verifying with the wrong secret. Common in dev environments where the JWT was issued by a staging auth server but verified against a local secret. The fix is to make sure both ends use the same key.
- The algorithm changed. A token signed with HS256 cannot be verified as RS256, even with the right key material. Look at the
algfield in the header and confirm your verifier matches.
Never paste a real production token with its signature into an online decoder you don't trust — anyone who copies the full token can replay it until it expires. Our decoder is browser-only by design; nothing ever leaves your device.
Error 3 — "jwt malformed"
The token isn't even shaped like a JWT. Some causes:
- Missing
Bearerprefix or extra spaces. The Authorization header should beAuthorization: Bearer eyJ...— exactly one space, no quotes around the token. - The token was URL-encoded twice. If you see
%2Einstead of.in the token, your client encoded it once on its way into a query parameter and the server is decoding it once instead of twice. - Truncation. Some HTTP clients truncate long headers. JWTs are typically 200–800 characters; if yours is being cut at 256, you'll get a malformed-token error.
- Wrong field entirely. Common when developers copy the
access_tokenresponse and accidentally use theid_tokenor refresh token as the bearer.
The first sanity check: a real JWT has exactly two dots, dividing it into three base64url chunks. Zero dots or one dot means it's not a JWT.
Error 4 — "jwt not active" / "token not yet valid"
The current time is before the nbf (not before) claim. Almost always a clock skew issue: your auth server set nbf to its current time, but your API server's clock is several seconds behind.
Decode the token, look at nbf, compare to now() on the verifying server. If nbf is even one second in the future, strict verifiers will reject. Allow a 5–10 second skew window, and run NTP everywhere.
Reading the payload — what to look for
When debugging an authorization failure that isn't one of the four above, the answer usually lives in the payload. Decode the token and check:
sub— is this actually the user you think it is? Mistakes here usually mean a token cache key collision in your client.aud(audience) — many APIs reject tokens whose audience doesn't match the API's expected identifier.scopeorpermissions— does the token actually have permission for the endpoint you're calling? "401 expired" is sometimes a misleading error for "scope not granted".- Custom claims — most production tokens carry custom data like
tenant_id,role, ororg_id. If your service expects one of these and it's missing, you'll see authorization errors that look generic.
JWT payloads are JSON — and JSON has its own gotchas
Once a JWT decoder shows you the payload, you're back in JSON land. If the payload looks structurally weird — duplicate keys, unexpected types, missing fields — the issue may be on the issuer side. The same parsing rules apply: if you're hand-crafting tokens for tests, every claim must be valid JSON. Our writeup on JSON syntax errors covers the common pitfalls.
A quick security checklist when handling JWTs
- Never log full tokens. Logging an access token is logging a credential. Log the
jticlaim or a hash of the token instead. - Reject
alg: none. Some libraries historically accepted unsigned tokens ifalgwas set tonone. Modern libraries reject this; if yours doesn't, upgrade. - Store on the client carefully.
localStorageis XSS-readable.HttpOnlycookies are safer for session tokens, with appropriateSameSiteandSecureflags. - Short access tokens, longer refresh tokens. A 15-minute access token limits the blast radius of a leak; a 7-day refresh token kept in an HttpOnly cookie keeps the user logged in.
The first thing to try, every time
When a JWT-protected API returns 401, before reading any logs: paste the token into a decoder, look at exp and iat, and confirm the token is fresh. Nine times out of ten the "auth bug" is a token that quietly aged out twenty minutes ago. The other ten percent is one of the four errors above — and once you've decoded the payload, the cause is usually obvious.