How to decode a JWT token: complete guide for developers
Your API just returned 401 Unauthorized. The Authorization: Bearer eyJhbGciOi... header looks fine. Was the token expired, was the audience wrong, or did the issuer rotate a key? You cannot answer that question without reading what is actually inside the token, and to decode a JWT token you do not need a secret, a library, or even a network connection. A JWT is three base64url-encoded chunks joined by dots. Decoding it is mechanical: split, base64url, JSON.parse. No magic, no cryptography.
This guide walks through the anatomy, shows you how to decode a JWT in Node.js, Python, Go, and the browser, covers the decode-vs-verify distinction that trips up most teams, and lists the real failure modes that will bite you. If you just need to inspect a token right now, jump to our free JWT decoder. It runs entirely in your browser, so production tokens never leave your device.
What is a JWT? (Quick anatomy)
A JSON Web Token (JWT) is a compact, URL-safe credential defined in RFC 7519. It carries claims, meaning data about the user and the token itself, between two parties. A JWT is three base64url-encoded pieces joined by dots: a header, a payload, and a signature.
Here is a real token broken apart so you can see the structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0 ← payload
.
4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0 ← signature
The header describes how the token is signed, usually { "alg": "HS256", "typ": "JWT" }. The payload carries the claims: registered ones like sub, exp, iat plus custom ones like role or tenant. The signature is a cryptographic proof computed over the header and payload that lets the recipient detect tampering. Base64url is a URL-safe variant of base64; see our beginner-friendly Base64 guide for the ten-minute primer.
You will meet JWTs everywhere modern auth lives: OAuth 2.0 access tokens, OpenID Connect ID tokens, API credentials minted by Auth0, Okta, Clerk, Supabase, Firebase, and tokens passed between microservices inside a mesh. They are the default credential format of the last decade.
Before we go further, one line you must internalize: JWTs are encoded, not encrypted. Anyone who holds the token can read every claim. The signature proves origin; it does not hide the contents. That one fact dictates what follows: what is safe to put in the payload, why decoding needs no secret, and why signature verification is non-negotiable on the server side.
How JWT decoding works (base64url, not decryption)
Decoding a JWT is not a cryptographic operation. It is four mechanical steps:
- Split the token on
.into exactly three segments. - base64url-decode the first segment and parse it as JSON. That is the header.
- base64url-decode the second segment and parse it as JSON. That is the payload.
- Leave the third segment (the signature) as raw bytes. Verifying it requires the key.
That is the entire algorithm. No library is mandatory. Any language with base64 and a JSON parser can decode a JWT in five lines. Our Base64 encoder/decoder will do steps 2 and 3 by hand if you want to see the mechanics.
What is base64url?
Base64url is plain base64 with three tweaks so the output is safe in URLs and HTTP headers: - replaces +, _ replaces /, and trailing = padding is dropped. If you feed raw base64url into a standard base64 decoder without reversing those substitutions, you get either garbage or an error. The advanced Base64 guide covers the padding edge cases in depth.
| Standard base64 | base64url | |
|---|---|---|
| Alphabet | A-Z a-z 0-9 + / | A-Z a-z 0-9 - _ |
| Padding | Required = at end | Dropped |
| URL safe? | No | Yes |
| Example | PDw/Pz8+ | PDw_Pz8- |
One more thing worth saying out loud: you cannot decrypt the signature client-side. Decoding is one-way from encoded bytes to JSON. Verifying the signature is a separate operation that needs either the HMAC secret (for HS-family algorithms) or the issuer’s public key (for RS, PS, ES, EdDSA).
Why you don’t need the secret to decode
Because the payload is base64url plus JSON, not ciphertext. A secret only enters the picture when you want to prove the token has not been tampered with, which is a signature check. Anyone on the network path, anyone holding the token in a log line, anyone with a browser can read every claim you put into it. That is why you must never put passwords, API keys, or PII beyond what the recipient already knows inside a JWT payload. For the broader threat model, read our security best-practices guide.
Decode JWT online in 3 clicks: free JWT decoder
Sometimes you just need an answer right now. Is this token expired, is the aud claim what I think it is, does the header say alg:none? The fastest path is our online JWT decoder. It is built for the 2 a.m. incident response path.
- Paste the full token into the input area. Include all three dot-separated segments.
- Read the decoded header, payload, and the status chips at the top: algorithm, issued-at, expiration, and a red
Expiredbadge ifexpis already in the past. - Copy whichever panel you need into your bug report, Slack thread, or test fixture.
Why it is safe for real production tokens:
- 100% browser-based. Decoding runs via native
atobandJSON.parse. No network request, ever. - No logging, no tracking, no cookies, no signup.
- Works offline once the page has loaded.
The JWT Decoder tool is algorithm-agnostic. Because decoding only needs base64url and JSON, it reads every JWS variant: HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA, and alg:none. Only signature verification depends on the algorithm, and verification is not something you want a public web tool doing. More on that in a moment.
Need to base64-decode a segment manually to cross-check the tool? Use our Base64 encoder/decoder and feed each segment in as base64url.
How to decode JWT in code (Node.js, Python, Go, browser)
For everything that is not interactive debugging, meaning middleware, tests, migration scripts, CLI tools, you will reach for a library. Here is the minimum code to decode a JWT in the four environments you are most likely to hit, with both the read-only path and the verification path side by side. Every snippet is copy-pasteable and produces the outputs shown in the comments.
Decode JWT in Node.js (jsonwebtoken)
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' +
'.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0' +
'.4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0';
// Decode only — does NOT verify the signature
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', exp: 1999999999 }
// Verify — the production path
const secret = process.env.JWT_SECRET;
const verified = jwt.verify(token, secret, { algorithms: ['HS256'] });
Always pass an explicit algorithms allowlist to verify. Omitting it has historically let attackers downgrade an RS256 token to HS256 by signing with the public key as an HMAC secret, the classic algorithm-confusion attack. The allowlist is your defense.
Decode JWT in Python (PyJWT)
# pip install PyJWT
import jwt
token = (
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0"
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
)
# Decode only — unsafe for auth, fine for inspection
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded) # {'sub': 'user_123', 'exp': 1999999999}
# Header without touching the payload
header = jwt.get_unverified_header(token)
print(header) # {'alg': 'HS256', 'typ': 'JWT'}
# Verify — production path
payload = jwt.decode(
token,
key="your-hs256-secret",
algorithms=["HS256"],
audience="api.example.com",
)
PyJWT refuses to verify without an algorithms list, a sensible default that prevents the same confusion attack the Node example warns about.
Decode JWT in Go (golang-jwt/jwt/v5)
// go get github.com/golang-jwt/jwt/v5
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0" +
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
// Decode only
parser := jwt.NewParser()
claims := jwt.MapClaims{}
_, _, err := parser.ParseUnverified(tokenString, claims)
if err != nil {
panic(err)
}
fmt.Println(claims["sub"], claims["exp"]) // user_123 1.999999999e+09
// Verify
secret := []byte("your-hs256-secret")
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
}
return secret, nil
})
fmt.Println(token.Valid, err)
}
The keyFunc closure is where you enforce the algorithm family. Reject anything that is not the method you expect before handing back the key.
Decode JWT in the browser (zero dependencies)
Sometimes you do not want a dependency at all: a quick debug panel, a browser extension, a tiny UI badge that shows the current user’s role. Native browser APIs are enough.
function decodeJwt(token) {
const [h, p] = token.split('.');
const pad = (s) => s + '==='.slice((s.length + 3) % 4);
const decodeSegment = (s) => {
const b64 = pad(s).replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
};
return { header: decodeSegment(h), payload: decodeSegment(p) };
}
const { header, payload } = decodeJwt(token);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: 'user_123', exp: 1999999999 }
The TextDecoder pass matters for any token with a non-ASCII payload (emoji in a display name, Cyrillic in a preferred_username). Plain atob returns a binary string, which breaks JSON.parse on multi-byte UTF-8. This is exactly what our online JWT decoder runs locally in your browser, minus the UI.
Comparison table
| Language | Decode-only | Verify | Library |
|---|---|---|---|
| Node.js | jwt.decode(token) | jwt.verify(token, key, { algorithms: [...] }) | jsonwebtoken |
| Python | jwt.decode(token, options={"verify_signature": False}) | jwt.decode(token, key, algorithms=[...]) | PyJWT |
| Go | parser.ParseUnverified(token, claims) | jwt.Parse(token, keyFunc) | golang-jwt/jwt/v5 |
| Browser | atob + TextDecoder + JSON.parse | Ask your backend | — |
Decode vs verify: the critical difference
Decoding a JWT reads its claims; verifying a JWT proves those claims were not tampered with. Decoding never trusts; verification is the trust. This is the single distinction that separates a working auth implementation from a CVE.
| Decode | Verify | |
|---|---|---|
| Needs secret/key? | No | Yes |
| Runs client-side? | Safely | Never |
| Proves authenticity? | No | Yes |
| Checks expiration? | Optional | Yes |
| Use case | Debugging, inspection | Authentication, authorization |
Never make an authorization decision from a decoded (unverified) JWT. Not in middleware, not in a React hook, not in a serverless function you think is behind the gateway. Decoded claims say what the token claims; verified claims say what the issuer signed. An attacker handing your server a hand-crafted token without a valid signature can put anything they want in the payload, and only the signature check rejects it.
One more detail on the HMAC family: if you use HS256, your secret’s entropy is the whole game. A short, guessable secret gets brute-forced offline against any token an attacker captures, and then they mint their own tokens and walk in the front door. Use at least 256 bits of real randomness. See our HMAC secret strength guide for the arithmetic on why this matters.
Common JWT claims reference
Every JWT you meet uses some subset of the registered claims from RFC 7519. Memorize the short list:
| Claim | Name | Example | Notes |
|---|---|---|---|
iss | Issuer | https://auth.example.com | Who minted the token |
sub | Subject | user_123 | Usually the user ID |
aud | Audience | api.example.com | Who the token is for — must match on the server |
exp | Expiration | 1715003600 | Unix seconds; past = expired |
iat | Issued At | 1715000000 | Unix seconds the token was minted |
nbf | Not Before | 1715000060 | Earliest time the token is usable |
jti | JWT ID | d1f8… | Unique per token; prevents replay |
kid | Key ID (header) | key-2025-01 | Which key in your JWKS signed this |
Application-specific claims sit alongside these: role, scope, email, tenant_id, whatever your identity provider emits. Keep them short. Every byte rides along on every request.
To read human dates from iat and exp, try our Unix timestamp converter. Paste the number, get the date in your local time zone, and spot a skew bug in a second.
Troubleshooting: why won’t my JWT decode?
Five real failure modes, in rough order of frequency. Each one is framed Symptom → Cause → Fix.
- “Invalid JWT format, expected three segments.” You copied only the payload, or the shell wrapped the token across lines and you picked up only the first line. Fix: recopy the full
xxx.yyy.zzzvalue from the original response body, not from a rendered terminal. Long single-line values survive better in a browser devtools Network tab than in a scrolled terminal. - Five segments instead of three. You have a JWE (encrypted JWT), not a JWS. The format is
header.encryptedKey.iv.ciphertext.tag. A decoder will read the header, but the payload is ciphertext. Fix: decoding the payload requires the decryption key, usually handled server-side by your auth SDK, not a debug tool. - Base64url error on an otherwise valid-looking token. The token was URL-encoded somewhere in the copy path (a cookie, a redirect URL, a captured proxy log). You will see literal
%2Eor%2Bin the string. Fix: URL-decode it first, then feed the result into the JWT decoder. - JSON parse error on the payload. A terminal or chat client inserted soft-wrap newlines, or a script pasted smart quotes around an identifier. Fix: view the raw response bytes (curl with
-o file.txt, or devtools’ Raw view), strip whitespace, and paste again. - Decodes cleanly but the backend still rejects it. Not a decoding problem, a verification problem. The token is structurally valid; something the server checks (signature,
aud,exp, clock skew,kidlookup) is failing. Jump to the next section.
Two honorable mentions that are not parse errors but are worth catching while you have the decoder open: an alg value of none in the header (treat as hostile in production) and an exp value in the past. The decoder still shows the claims so you can debug, which is correct behavior, and our tool flags it with a red Expired badge.
When decoding isn’t enough: signature verification
Decoding ends at “here is what the token claims.” Verification is what turns a claim into a trust decision. The signature is a proof, computed with the issuer’s private key or shared secret, that ties the header and payload together. Change a single byte and the signature check fails. Without that check, anyone who can POST to your endpoint can hand-craft an “admin” token by editing the payload and skipping the signature entirely.
Never accept alg:none.
Production verification, in every language and framework, looks roughly like this checklist. Treat missing items as bugs:
- Pass an explicit
algorithms: ['RS256'](or whatever you use) allowlist. This defeats algorithm-confusion attacks. - Verify
audmatches your service’s identifier andissmatches your expected issuer URL. - Check
expagainst the current time with at most 60 seconds of clock-skew tolerance. - On key rotation, look up the public key by
kidfrom a JWKS endpoint. Never hardcode a single key forever. - Revoke effectively by keeping
expshort (minutes, not days) and optionally maintaining ajtidenylist for high-value tokens.
Every mainstream JWT library exposes these as options on a single call. If your verification code does not set them, you are leaving defaults on, and defaults have historically been the bug. For the full threat model, see our security best-practices guide.
FAQ
Can I decode a JWT without the secret key?
Yes. The header and payload are base64url-encoded, not encrypted, so anyone holding the token can read its claims. The secret or public key is only required to verify the signature. This is by design: the payload is meant to be readable so the recipient can make authorization decisions.
Is it safe to paste my production JWT into an online decoder?
Only if the decoder runs in your browser and never uploads the token. Our JWT Decoder parses locally with native atob and JSON.parse; nothing is sent to any server. Remote debuggers that POST your token to an API should be treated as credential leaks.
What’s the difference between decoding and verifying a JWT?
Decoding just reads the claims. It needs no key and proves nothing. Verifying checks the signature against the issuer’s key and confirms the token has not been tampered with. Never make an authentication decision from a decoded but unverified token.
My JWT looks truncated. What counts as a valid format?
A valid JWT has exactly three base64url segments separated by dots: header.payload.signature. Five segments means you have an encrypted JWT (JWE), not a JWS. Zero dots means you copied only one segment from a wrapped terminal line.
Why does the decoder still show an expired token?
A decoder reads the claims regardless of validity so you can debug rejections. Only a verifier refuses expired tokens. Our tool surfaces an Expired badge by comparing exp against your local clock, so you can spot the problem instantly without squinting at Unix timestamps.
Which algorithms can I decode?
All of them. Decoding only needs base64url and JSON parsing, so it is algorithm-agnostic. That includes HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA, and alg:none. Only verification depends on the algorithm you chose.
Should I use jwt-decode or jsonwebtoken in Node.js?
Use jwt-decode on the frontend when you only need to read the payload, for example showing a username from an access token. Use jsonwebtoken on the backend, because only the backend can hold the signing key and perform jwt.verify. Never verify on the client.
Conclusion
Decoding a JWT is not as mysterious as “cryptographic token” suggests. Five takeaways and you will never be stuck staring at an opaque eyJhbGciOi… string again:
- Decoding is base64url plus JSON parse. No secret required.
- A JWT has three parts (header, payload, signature) joined by dots.
- Decoding never proves authenticity. Always verify server-side with the issuer’s key.
- Reject
alg:noneand always pass an explicit algorithm allowlist toverify. - Never store passwords, private keys, or sensitive PII in the payload. It is readable by anyone who holds the token.
Bookmark our free JWT decoder for on-call debugging. Paste a token, read the claims, and spot expiries in a second, all without your token ever leaving the browser.