A JWT has three dot-separated parts
A JSON Web Token is a single compact string with three base64url-encoded segments joined by dots: header.payload.signature. The first two segments are JSON objects; the third is a binary signature. They are not encrypted — they are signed, so anyone holding the token can read the header and payload.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcwNTMxMjIwMCwiZXhwIjoxNzA1Mzk4NjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────────── header ───────────┘ └──────────────────── payload ────────────────────┘ └──────────── signature ───────────┘Decode each segment with base64url
The header and payload are base64url-encoded, not standard base64: they use -and _ instead of + and /, and drop the trailing= padding. Decoding the example above gives:
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{
"sub": "user_123",
"role": "admin",
"iat": 1705312200,
"exp": 1705398600
}If you only have a base64 string and want to inspect it on its own, use the Base64 encoder/decoder. For a full walkthrough of the JWT format, see the guide to decoding a JWT.
Standard payload claims worth knowing
The payload carries claims — assertions about the user or session. Six registered claims (defined in RFC 7519) cover almost every debugging scenario:
sub— subject: the user or entity the token is aboutiat— issued at: Unix timestamp when the token was createdexp— expiration: Unix timestamp after which the token is invalidiss— issuer: who minted the token (e.g. your auth server or Auth0)aud— audience: which service the token is intended fornbf— not before: a time before which the token must be rejected
When an authenticated request returns a 401, decode the token and check these first: anexp in the past means it expired, a wrong aud means it belongs to a different API, and an unexpected sub means you grabbed the wrong user's token.
Security: the browser only decodes, it never verifies
This is the single most important thing to understand about decoding. Unpacking the base64url encoding tells you what the token claims, but proves nothing about who issued it or whether it was tampered with. Verification needs a key the browser does not have — the shared secret for HS256, or the issuer's public key forRS256/ES256 — and must run server-side.
- Never make an authorization decision from a payload you only decoded.
- A production JWT is a live credential — an untrusted online tool may log it and replay it before
exp. - Anyone with the token can read the payload, so never put secrets in it.
Jsonic's decoder runs entirely client-side — your token never leaves the browser — which makes it safe for development and staging tokens. For sensitive production tokens, decoding locally in DevTools (atob(token.split('.')[1])) is always the safer default.
Decode a JWT in code
When you need decoding inside a script or backend instead of the browser tool:
# Python — PyJWT, decode without verifying the signature
import jwt
claims = jwt.decode(token, options={"verify_signature": False})
print(claims) # {'sub': 'user_123', 'role': 'admin', 'iat': ..., 'exp': ...}// Node.js — jsonwebtoken, decode (does NOT verify)
const jwt = require('jsonwebtoken')
const decoded = jwt.decode(token, { complete: true })
console.log(decoded.header) // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload) // { sub: 'user_123', exp: 1705398600, ... }// Pure JS — no dependency, base64url by hand
function decodeJwtPart(part) {
const base64 = part.replace(/-/g, '+').replace(/_/g, '/')
return JSON.parse(atob(base64))
}
const [header, payload] = token.split('.')
decodeJwtPart(header) // → { alg: 'HS256', typ: 'JWT' }
decodeJwtPart(payload) // → { sub: 'user_123', ... }Checking expiry from the exp claim
The exp claim is a Unix timestamp in seconds, not milliseconds — a common off-by-1000 bug. Convert it to a date with new Date(exp * 1000) and compare against Date.now() / 1000 to decide whether the token has expired.
// exp: 1705398600
new Date(1705398600 * 1000).toISOString()
// → "2024-01-16T10:30:00.000Z"
const isExpired = Date.now() / 1000 > payload.expThis decoder shows exp (and iat) as human-readable dates automatically. For the deeper background — algorithm comparison, OAuth provider tokens, and when to verify instead of decode — read the how to decode a JWT guide.