How to Decode a JWT Token (And Verify It Correctly)
You are staring at a string that looks like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoibWFyaWFAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NDYwMDAwMDAsImV4cCI6MTc0NjA4NjQwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c. It is a JWT — a JSON Web Token — and every API, OAuth flow, and authentication system you work with produces them. This guide explains exactly what a JWT contains, how to decode it in under 30 seconds, what each field means, and how to verify one correctly in code.
What Is a JWT?
A JSON Web Token is a compact, URL-safe token format defined in RFC 7519. It is used to securely transmit claims — statements about a user or system — between parties. JWTs are the standard format for authentication tokens in modern web applications, OAuth 2.0 flows, and API authorisation systems.
The key property that makes JWTs useful: they are self-contained. The token itself carries all the information needed to verify its authenticity and read its claims, without requiring a database lookup on every request. A server can validate a JWT using only a public key or shared secret — no session store, no Redis call, no database query.
The Structure of a JWT
Every JWT has exactly three parts, separated by dots: header.payload.signature. Each part is Base64URL-encoded. The header and payload are just JSON — anyone can decode them. The signature is what makes the token tamper-proof.
Part 1: Header
The header is a JSON object describing the token type and the signing algorithm used.
// Base64URL decode: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{
"alg": "HS256", // Signing algorithm: HMAC-SHA256
"typ": "JWT" // Token type
}
Common algorithm values: HS256 (HMAC with SHA-256, shared secret), RS256 (RSA with SHA-256, public/private key pair), ES256 (ECDSA with SHA-256), none (no signature — dangerous, never use in production).
Part 2: Payload (Claims)
The payload contains the claims — the actual data the token carries. Claims are divided into three categories: registered (standardised), public (commonly used conventions), and private (custom to your application).
// Base64URL decode: eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoibWFyaWFAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NDYwMDAwMDAsImV4cCI6MTc0NjA4NjQwMH0
{
"sub": "user_123", // Subject — who the token is about
"email": "maria@example.com", // Public claim
"role": "admin", // Private claim (custom)
"iat": 1746000000, // Issued At (Unix timestamp)
"exp": 1746086400 // Expiration (Unix timestamp)
}
Registered Claim Names You Will See
| Claim | Full name | Meaning |
|---|---|---|
sub | Subject | Who the token is about — typically a user ID |
iss | Issuer | Who issued the token — your auth server URL |
aud | Audience | Who the token is intended for — your API URL |
exp | Expiration | Unix timestamp after which token is invalid |
iat | Issued At | Unix timestamp when token was issued |
nbf | Not Before | Unix timestamp before which token is invalid |
jti | JWT ID | Unique identifier for this specific token |
Part 3: Signature
The signature is computed over the encoded header and payload using the algorithm specified in the header. For HS256: HMAC-SHA256(base64url(header) + "." + base64url(payload), secret). For RS256: RSA-SHA256(base64url(header) + "." + base64url(payload), privateKey).
The signature is what prevents tampering. If anyone modifies the header or payload, the signature no longer matches, and the token is rejected. Anyone can decode the header and payload — they are not encrypted. Only the signature provides security.
How to Decode a JWT Instantly
Decoding a JWT means reading the header and payload. This requires no key and no secret — you just Base64URL-decode each part. Use ToolPry's Base64 Decoder: paste the header or payload section (everything between the dots), and the decoded JSON appears immediately.
For the expiry time specifically, the exp claim is a Unix timestamp. Paste it into ToolPry's Timestamp Converter to instantly see the human-readable expiry date and time.
Decoding JWTs in Code
JavaScript — decode without verification (inspection only)
function decodeJWT(token) {
const [header, payload, signature] = token.split('.');
// Base64URL → Base64 → decode
const decode = (str) => {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '=='.slice((base64.length + 3) % 4); // add padding
return JSON.parse(atob(padded));
};
return {
header: decode(header),
payload: decode(payload),
signature // raw — cannot be decoded without the key
};
}
const { header, payload } = decodeJWT(token);
console.log(payload.exp); // Unix timestamp
console.log(new Date(payload.exp * 1000).toISOString()); // human date
console.log(payload.exp > Date.now() / 1000); // true = not expired
JavaScript — verify and decode (production use)
// npm install jose (or jsonwebtoken for Node.js only)
import { jwtVerify, importJWK } from 'jose';
const secret = new TextEncoder().encode('your-secret-key'); // HS256
// or for RS256: const publicKey = await importJWK(jwk, 'RS256');
try {
const { payload } = await jwtVerify(token, secret, {
issuer: 'https://auth.example.com', // validates iss claim
audience: 'https://api.example.com', // validates aud claim
});
console.log(payload.sub); // user ID
console.log(payload.email); // email
} catch (err) {
// JWTExpired, JWTInvalid, JWSSignatureVerificationFailed, etc.
console.error('Token invalid:', err.code);
}
Python
import jwt # pip install PyJWT
# Decode WITHOUT verification (inspection only — never trust the claims)
header = jwt.get_unverified_header(token)
payload = jwt.decode(token, options={"verify_signature": False})
print(payload['exp'])
# Decode WITH verification (production use)
try:
payload = jwt.decode(
token,
key='your-secret-key', # or RSA public key for RS256
algorithms=['HS256'],
audience='https://api.example.com',
issuer='https://auth.example.com',
)
print(payload['sub'])
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
Common JWT Mistakes and Security Issues
Accepting the none algorithm. Some early JWT libraries accepted tokens with "alg": "none" — no signature required. This means an attacker could craft any payload and the library would accept it. Always specify the expected algorithm explicitly when verifying.
Confusing decoding with verification. Decoding reads the claims. Verification checks that the signature is valid. You must verify before trusting any claim in the payload. Never make authorisation decisions based on decoded-but-unverified JWT claims.
Storing sensitive data in the payload. The payload is not encrypted — it is only Base64-encoded. Anyone who intercepts the token can read every claim. Never put passwords, credit card numbers, PII beyond what is necessary, or any data you would not want exposed in a JWT payload.
Long expiry times. JWTs cannot be invalidated server-side without a token blocklist (which defeats part of their purpose). Set short expiry times: 15 minutes for access tokens, 7–30 days for refresh tokens. Use refresh token rotation to maintain sessions securely.
Storing JWTs in localStorage. localStorage is accessible to any JavaScript on the page, making tokens vulnerable to XSS attacks. Store access tokens in memory (a JavaScript variable) and refresh tokens in HttpOnly cookies that JavaScript cannot access.
Checking JWT Expiry Without a Library
function isTokenExpired(token) {
try {
const payload = JSON.parse(
atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
);
if (!payload.exp) return false; // no expiry claim = does not expire
return payload.exp < Math.floor(Date.now() / 1000);
} catch {
return true; // malformed token = treat as expired
}
}
// Usage
if (isTokenExpired(accessToken)) {
await refreshAccessToken();
}
Frequently Asked Questions
Is a JWT encrypted?
Standard JWTs (JWS — JSON Web Signature) are signed but not encrypted. The payload is only Base64URL-encoded, which is trivially reversible. Anyone who obtains the token can read its claims. JWE (JSON Web Encryption) is a separate standard that encrypts the payload — you can identify these because they have five parts instead of three. In practice, most JWTs are JWS tokens transmitted over HTTPS, which provides transport-layer encryption.
How do I invalidate a JWT before it expires?
You cannot invalidate a standard JWT server-side without maintaining state — which partially defeats their purpose. The common approaches: keep a token blocklist (a set of invalidated JTI values), use very short expiry times so the window of misuse is small, or rotate the signing secret (which invalidates all tokens but may not be practical). For logout flows, delete the token client-side and maintain a small server-side blocklist of recently issued tokens for the expected token lifetime.
What is the difference between a JWT and a session cookie?
A session cookie stores a session ID — a random opaque value that the server looks up in a database on every request to retrieve the user's data. A JWT stores the data itself in the token. Session cookies require a session store (database, Redis) but can be invalidated instantly. JWTs require no session store and scale horizontally without shared state, but cannot be easily invalidated before expiry. For most applications, session cookies with a database are simpler and safer; JWTs are advantageous in distributed systems and API contexts where stateless authentication matters.
Why does my JWT have four or five parts instead of three?
A four-part token is unusual and likely malformed. A five-part token is a JWE (JSON Web Encryption) token: header.encrypted_key.iv.ciphertext.tag. JWE tokens encrypt their payload, unlike standard JWTs. You need the private key to decrypt them — Base64 decoding the payload section will produce unreadable binary data, not JSON.