JWT Token Format: Understanding Header, Payload, and Signature
Every JWT is a string of three base64url-encoded segments separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIn0.
MEyUvM7mN3pQ8tHzQ4gk_5GqKjZ-hINHJmKX_JNyV8s
├───────────────────────────────┘
├────────────────────────────────────────────────────┘
Header Payload Signature
This article breaks down exactly what each part contains.
The Header
The header is a JSON object that describes the token:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-01"
}
alg (required): The signing algorithm. Common values: HS256 (HMAC + SHA-256), RS256 (RSA + SHA-256), ES256 (ECDSA + SHA-256), EdDSA (Edwards-curve Digital Signature Algorithm). A Stack Overflow question about algorithm selection has 75,000 views.
typ (optional but recommended): Set to JWT. Helps distinguish JWTs from other token types.
kid (optional): Key ID. Used when the server has multiple signing keys. The verifier uses kid to select the correct key from a set:
const keys = {
"key-2026-01": "-----BEGIN PUBLIC KEY-----\n...",
"key-2026-02": "-----BEGIN PUBLIC KEY-----\n..."
};
const decoded = jwt.verify(token, keys, { algorithms: ["RS256"] });
// jwt.verify auto-selects the key based on the kid claim
The Payload (Claims)
The payload contains claims — statements about the user and the token:
{
"sub": "user_8a7b3c2d",
"name": "Alice Johnson",
"iat": 1719561600,
"exp": 1719562500,
"roles": ["admin", "editor"]
}
Registered Claims (IANA)
| Claim | Full Name | Purpose |
|---|---|---|
iss | Issuer | Identifies who created the token |
sub | Subject | Identifies the user |
aud | Audience | Identifies the intended recipient |
exp | Expiration | Unix timestamp when token expires |
nbf | Not Before | Token is not valid before this time |
iat | Issued At | When the token was created |
jti | JWT ID | Unique identifier for this token |
Important: The payload is base64url-encoded, not encrypted. Anyone can decode it using atob():
function decodeJWTPayload(token) {
const parts = token.split(".");
if (parts.length !== 3) throw new Error("Invalid JWT");
const payload = parts[1];
// Pad base64url to base64
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
return JSON.parse(atob(padded));
}
const token = "eyJzdWIiOiIxMjMifQ.eyJzdWIiOiIxMjMifQ.signature";
console.log(decodeJWTPayload(token));
// { sub: "123" }
A post on r/javascript reminding developers that JWT payloads are not encrypted has over 1,200 upvotes. The most common mistake is storing API keys or passwords in JWT claims.
The Signature
The signature is created by:
- Taking the base64url-encoded header + "." + base64url-encoded payload
- Signing that string with the chosen algorithm and key
// What happens inside jwt.sign():
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
const payload = base64url(JSON.stringify({ sub: "123" }));
const signingInput = header + "." + payload;
// RS256: sign with RSA private key
const signature = rsaSign(signingInput, privateKey, "sha256");
const token = signingInput + "." + base64url(signature);
If even a single character of the header or payload is modified, the signature verification fails.
Putting It Together: A Real Example
Using the DevFormatters JWT Decoder, here is what a real JWT looks like internally:
// Full JWT
const token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJzdWIiOiJ1c2VyXzhhN2IzYzJkIiwiaWF0IjoxNzE5NTYxNjAwLCJleHAiOjE3MTk1NjI1MDAsInJvbGVzIjpbImFkbWluIiwiZWRpdG9yIl19."
+ "MEyUvM7mN3pQ8tHzQ4gk_5GqKjZ-hINHJmKX_JNyV8s";
// Decoded header
// { "alg": "RS256", "typ": "JWT" }
// Decoded payload
// {
// "sub": "user_8a7b3c2d",
// "iat": 1719561600,
// "exp": 1719562500,
// "roles": ["admin", "editor"]
// }
You can paste any JWT into the JWT Decoder on DevFormatters to see its decoded contents instantly.
Related Searches
- jwt token structure explained
- jwt header payload signature
- jwt base64url encoding
- jwt registered claims
- jwt kid key id
- how jwt signature works
- decode jwt without library
- jwt token format rfc
- jwt payload claims list
- jwt algorithm types
Frequently Asked Questions
Can I read a JWT payload without the secret key?
Yes. The payload is base64url-encoded, not encrypted. Any tool can decode it. This is why you must never store sensitive data (passwords, API keys, PII) in JWT claims. The signature prevents tampering, but anyone can read the payload.
What happens if a JWT has fewer or more than three segments?
A valid JWT always has exactly three segments separated by dots. Fewer segments means it is not a complete JWT. More segments means something is wrong (possibly an injection attempt). Always validate the segment count before processing.
What is the maximum size of a JWT?
HTTP headers have size limits (typically 8-16 KB). The JWT, which is sent in the Authorization header, must fit within these limits. A payload of about 5-7 KB of claims is the practical maximum. Keep your JWTs small — user ID and roles only.
How do I verify the signature of a JWT?
Use jwt.verify() with the correct key and algorithm. The process: (1) decode the base64url-encoded header and payload, (2) recompute the signature using the algorithm from the header, (3) compare the computed signature with the provided signature. If they match, the token is valid.
What is the difference between JWT and JWE?
JWT (JSON Web Token) is signed but not encrypted — anyone can read the payload. JWE (JSON Web Encryption) encrypts the payload so only the intended recipient can read it. JWE is rarely used in practice because most JWT communications happen over TLS, which already encrypts the transport layer.
Final Thoughts
Understanding JWT's three-part structure — header, payload, signature — helps you debug authentication issues faster, avoid security pitfalls (like storing secrets in the payload), and choose the right claims for your use case. The signature is the only part that provides security; the header and payload are always readable.