How to Decode a JWT (and Why Decoding Is Not Verifying)
Decode a JWT in your browser: split the header, claims and signature, read exp and aud, and learn why decoding a token is not the same as verifying it.
To decode a JWT, split the token on its two dots, base64url-decode the first part (the header) and the second part (the claims), and parse each as JSON. The third part is the signature, which decoding ignores. That is the catch: decoding shows what a token says, not whether it is genuine. The free JWT decoder does this in your browser.
What is inside a JWT?
RFC 7519 (May 2015) defines a JSON Web Token as "a compact, URL-safe means of representing claims to be transferred between two parties." In practice almost every JWT you see is a signed token (JWS) with three base64url parts separated by periods:
- Header: the algorithm (
alg) and usually the type (typ), sometimes a key ID (kid). - Payload: the claims, a JSON object about the user, the issuer and the token's lifetime.
- Signature: computed over
header.payloadwith a secret or private key.
Here is a real token, signed with HS256 and a demo secret for this article:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzg0MTIiLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20iLCJpYXQiOjE3OTA4MTI4MDAsImV4cCI6MTc5MDgxNjQwMCwic2NvcGUiOiJvcmRlcnM6cmVhZCJ9.TefmnHYeHz-Ht2hIMbramcfRwxr3uIKH_sbD6NDoUVADecoded, the first two parts are plain JSON:
// Header
{ "alg": "HS256", "typ": "JWT" }
// Payload
{
"iss": "https://auth.example.com",
"sub": "user_8412",
"aud": "api.example.com",
"iat": 1790812800,
"exp": 1790816400,
"scope": "orders:read"
}Every JWT header starts with eyJ because that is how {" looks in base64url. It is a quick way to spot a token in logs.
How do I decode a JWT in JavaScript?
You do not need a library to decode. In Node.js, Buffer understands base64url directly:
const [h, p] = token.split(".");
const header = JSON.parse(Buffer.from(h, "base64url").toString());
const payload = JSON.parse(Buffer.from(p, "base64url").toString());In the browser, atob only accepts standard base64, so convert first: replace - with +, _ with /, and pad with = to a multiple of four. The JWS spec drops padding, which is why a naive atob(part) throws on many tokens. Decode the bytes with TextDecoder so non-ASCII names survive. That is exactly what the site's JWT decoder does.
What do the standard JWT claims mean?
RFC 7519 registers seven claim names. None is mandatory, but most auth servers send the first six.
| Claim | Meaning | Example value above |
|---|---|---|
iss | Issuer: who created the token | https://auth.example.com |
sub | Subject: who the token is about | user_8412 |
aud | Audience: who should accept it | api.example.com |
exp | Expiration time | 1790816400 = 2026-10-01T01:00:00Z |
nbf | Not before | Not set |
iat | Issued at | 1790812800 = 2026-10-01T00:00:00Z |
jti | Unique token ID, for replay protection | Not set |
Time claims are a NumericDate, which RFC 7519 defines as "the number of seconds from 1970-01-01T00:00:00Z UTC." Seconds, not milliseconds: use new Date(exp * 1000). The scope claim is not registered; it is a private claim agreed between the issuer and your API.
Why is decoding a JWT not the same as verifying it?
Decoding is a formatting step. Anyone can do it, and anyone can do the reverse. To prove the point, we changed sub to "admin" and re-encoded the payload. The forged token decodes perfectly and shows admin. Only the signature check fails, because the attacker does not have the key.
So a decoded payload is untrusted input until you verify it. Verification means checking the signature with the expected key and an allow-listed algorithm, then checking the claims. RFC 8725, the JWT Best Current Practices (BCP 225, 2020), is direct about it: libraries "MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms." That rule blocks the classic "alg": "none" and algorithm-confusion attacks.
| Decoding | Verifying | |
|---|---|---|
| Needs a key | No | Yes (secret or public key) |
| Detects tampering | No | Yes |
Checks exp, aud, iss | Only shows them | Enforces them |
| Use for | Debugging, reading claims in a UI | Every authorization decision |
How do I verify a JWT properly?
Use a maintained library on the server, never hand-rolled code. With the widely used jose package in Node.js, verification and claim checks are one call:
import { jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret, {
algorithms: ["HS256"],
issuer: "https://auth.example.com",
audience: "api.example.com",
});That covers the RFC rules. For exp, the token "MUST NOT be accepted for processing" on or after the expiry time, with a small leeway, "usually no more than a few minutes," for clock skew. For aud, if your service is not in the audience, "the JWT MUST be rejected." Tokens issued for another API should never work on yours.
Is a JWT encrypted?
Usually not. A signed JWT guarantees integrity, not secrecy. RFC 7519 states that "support for encrypted JWTs is OPTIONAL," and most identity providers issue signed-only tokens. Treat everything in the payload as visible to the user, their browser extensions and your logs. Keep personal data to a minimum, and never put secrets in claims.
Once decoded, the payload is plain JSON. You can pretty-print it with the JSON tools or check its shape against a schema; our JSON Schema 2020-12 guide shows how. If you pass decoded claims or API payloads to a model, formatting REST API responses for LLMs covers keeping them compact, and fixing invalid JSON from LLMs covers the reverse direction.
Frequently Asked Questions
Can anyone decode a JWT?
Yes. A normal signed JWT is only base64url-encoded, not encrypted, so anyone holding the token can read the header and claims. Never put passwords, secrets or sensitive personal data in a JWT payload. If the contents must be confidential, use an encrypted JWT (JWE), which RFC 7519 lists as optional.
What is the difference between decoding and verifying a JWT?
Decoding turns the base64url header and payload back into JSON so you can read them. Verifying recomputes or checks the signature with the right key and an allowed algorithm, then checks exp, nbf, iss and aud. Only a verified token proves the claims came from the issuer and were not changed.
How do I read the exp claim in a JWT?
exp is a NumericDate: seconds since 1970-01-01T00:00:00Z UTC. Multiply by 1000 and pass it to new Date() in JavaScript. RFC 7519 says the token must not be accepted on or after that time, and allows a small leeway, usually no more than a few minutes, for clock skew.
Is it safe to paste a JWT into an online decoder?
Only if decoding happens locally. A live access token is a bearer credential, so anyone who receives it can use it until it expires. The json2toon.co JWT decoder decodes in your browser without uploading the token. For production tokens, prefer local tools or expired test tokens.
Recommended Reading
How to Convert XML to JSON Without Losing Attributes
Convert XML to JSON without losing data: plan for attributes, repeated elements, mixed content and namespaces. Mapping rules, examples and a free converter.
Fix Invalid JSON From LLMs: Truncation, Commas, Quotes
Fix invalid JSON from LLMs: truncated output, trailing commas, single quotes and code fences. How to repair it, when to use structured outputs, and a free tool.
How to Convert JSON to CSV (Including Nested JSON)
Convert JSON to CSV correctly: flatten nested objects, quote commas and line breaks per RFC 4180, handle arrays, and open the result cleanly in Excel.