How to Decode a JWT Token:
Step-by-Step Tutorial
Decode any JWT to inspect its header, payload, and expiry — then read the five claims that explain nearly every 401 error. Decoding is not verifying; learn both.
This tutorial shows you how to decode a JSON Web Token to see its header, payload, and expiry — the first move when debugging any authentication problem. Decoding takes seconds; the skill is in reading what comes out. One rule up front: decoding is not verifying. Anyone can read a JWT; only the signature check proves it's genuine.
Step 1: Get the Token
JWTs travel in the Authorization header, cookies, or local storage. Grab one from wherever your app keeps it:
- Browser DevTools → Network tab: click a request, find
Authorization: Bearer eyJ…in the request headers, copy everything afterBearer. - DevTools → Application tab: check Local Storage and Cookies for values starting with
eyJ. - From a curl session: it's the
access_tokenfield in the login response.
A JWT is three Base64url sections joined by dots — header.payload.signature:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
If your string doesn't have exactly two dots, it's not a JWT (or it's truncated — a very common copy/paste failure).
Step 2: Decode It
Paste the token into the JWT Decoder. Decoding happens entirely in your browser — the token is never uploaded, which matters because a live token is a credential. You'll see two JSON blocks:
Header — how the token is signed:
{
"alg": "HS256",
"typ": "JWT"
}
Payload — the claims:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The third section, the signature, can't be "decoded" — it's raw cryptographic bytes, meaningful only for verification.
Step 3: Read the Claims That Matter
Debugging auth issues almost always comes down to five registered claims:
exp— expiry, as a Unix timestamp. The #1 cause of sudden 401s. Convert it with the Timestamp Converter; if it's in the past, the token is dead and the client should refresh it.iat— issued-at. Compare withexpto learn the token lifetime (e.g.exp − iat = 3600means 1-hour tokens).nbf— not-before. A token used before this time is rejected; if it's in the future, check for clock skew between servers.sub— subject, usually the user ID. Confirms which user the token actually represents.iss/aud— issuer and audience. A 401 despite a fresh token often means the API expects a differentaudvalue than your auth server sets (common with misconfigured OAuth clients).
Worked Example: Debugging a 401 After Login
A user logs in successfully but every API call returns 401 Unauthorized. The five-minute diagnosis:
- Copy the token from the failing request's
Authorizationheader. - Decode it. Check
exp:1784540042→ converts to 20 minutes from now. Not expired. - Check
aud:"api.staging.example.com". But the failing request goes toapi.example.com— the frontend is pointed at production while logging in against staging. That's the bug. - No decoding tool would have found this faster: the server's only public statement was "401", and the token's payload contained the actual explanation.
If everything in the payload had looked correct, the next suspect would be the signature — wrong signing key on one side — which you can test in the JWT Inspector by pasting the shared secret (only ever do this with development secrets).
Decoding JWTs Without the Tool
The payload is just Base64url, so you can decode it anywhere:
# Command line — decode the payload (second section)
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" | base64 -d
# If you get an "invalid input" error, add padding: append == and retry
# Node.js
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64url')))" "eyJ...full.token"
# Python 3
python3 -c "import base64, json, sys; p=sys.argv[1].split('.')[1]; print(json.loads(base64.urlsafe_b64decode(p + '=' * (-len(p) % 4))))" "eyJ...full.token"
The padding dance (= characters) is needed because JWTs strip Base64 padding; the browser tool handles it silently. These one-liners are handy inside servers and CI where you can't open a browser.
Common Problems and Fixes
"Invalid token" on paste
Check for whitespace or line breaks introduced by copying from a terminal, a leading Bearer prefix that came along, or a truncated final section. The token must be one unbroken string with two dots.
Payload decodes but shows unreadable binary
Some systems issue JWE (encrypted JWTs, five sections instead of three) rather than JWS. Encrypted payloads can't be read without the decryption key — count the dots: 2 dots = decodable JWS, 4 dots = encrypted JWE.
Token looks perfect but the server still rejects it
The signature doesn't validate server-side: wrong secret/key, or an alg mismatch (server expects RS256, token says HS256). This is invisible to decoding — compare the alg header with the server's configuration, and verify the signature with the correct key in the JWT Inspector.
exp is valid but users get logged out anyway
Server clock skew, or a server-side revocation list. If exp minus current time is under ~60 seconds, some libraries reject preemptively. Check whether the backend applies a leeway setting.
FAQ
Is it safe to paste a JWT into an online decoder?
Into a client-side decoder like this one, yes — nothing leaves your browser. Still, treat live production tokens as credentials: prefer expired tokens or staging tokens when sharing screenshots or bug reports, because anyone holding a valid token can use it.
Why can I read the token without any key?
By design. A JWS token is signed, not encrypted — the signature makes tampering detectable but does nothing for confidentiality. Consequence: never put secrets in JWT payloads (passwords, API keys, personal data beyond what's needed). Anyone who sees the token reads them. For details on structure and signing algorithms, see the JWT deep dive.
Can I change the payload and re-use the token?
You can edit the payload, but the signature then no longer matches, and any correctly implemented server rejects it. That's the entire security model. (Historic implementations that accepted "alg": "none" were a famous vulnerability — modern libraries disable it.)
How do I create a JWT for testing?
Use the JWT Encoder: set the claims, pick HS256, sign with a throwaway development secret. Useful for seeding integration tests and trying out claim combinations without standing up an auth server.