How to Read and Decode a JWT Token
JSON Web Tokens (JWTs) are everywhere โ authentication cookies, API bearer tokens, OAuth flows. Yet many developers who use them daily have never manually looked inside one. This guide explains the structure, shows you how to decode any JWT by hand or with a tool, and highlights the security nuances that catch developers off guard.
The Three-Part Structure
A JWT always looks like three Base64url-encoded strings joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c The three parts are:
- Header โ algorithm and token type
- Payload โ claims (the actual data)
- Signature โ cryptographic proof of authenticity
The Header
Decode the first segment with Base64url and you get JSON like this:
{
"alg": "HS256",
"typ": "JWT"
} alg names the signing algorithm. Common values are HS256 (HMAC-SHA256, symmetric key), RS256 (RSA-SHA256, asymmetric), and ES256 (ECDSA-SHA256). The algorithm determines how the signature is generated and verified. typ is almost always JWT.
Security note: Older JWT libraries honoured the alg header when verifying signatures. An attacker could set "alg": "none" to bypass verification entirely. Always configure your library to accept only the specific algorithm(s) your system uses.
The Payload (Claims)
The second segment decodes to the claims object โ the token's actual data:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1716239022
} The JWT specification defines several registered claims that have standardised meanings:
subโ Subject: who the token represents (usually a user ID).issโ Issuer: which service issued the token.audโ Audience: which service(s) should accept the token.expโ Expiration: Unix timestamp after which the token is invalid.iatโ Issued At: Unix timestamp when the token was created.nbfโ Not Before: Unix timestamp before which the token must be rejected.jtiโ JWT ID: a unique identifier, useful for blacklisting individual tokens.
Applications add their own private claims alongside these โ things like role, email, permissions, or any data the server wants to embed for the client.
The Signature
The signature is computed over the encoded header and payload:
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret
) The signature lets any server with the correct secret (or public key, for RSA/ECDSA) verify that the token has not been tampered with. If an attacker modifies even one character of the payload, the signature will not match.
Critical point: The signature proves integrity (the token has not been modified) but not confidentiality (the payload is not encrypted). Anyone who receives a JWT can read the header and payload โ Base64 is reversible. Never put sensitive data like passwords or credit card numbers in a JWT payload.
How to Decode a JWT Manually
You can decode any JWT in a terminal without a library:
# Split on dots and decode each part (bash)
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.abc"
HEADER=$(echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null)
echo $HEADER | python3 -m json.tool
echo $PAYLOAD | python3 -m json.tool
Note that Base64url does not require padding (=), so the base64 -d command may need the padding added manually, or use a library that handles URL-safe Base64.
Checking Token Expiry
The exp claim is a Unix timestamp (seconds since 1970-01-01 UTC). Compare it against the current time:
# Bash
EXP=$(echo $PAYLOAD | python3 -c "import sys,json; print(json.load(sys.stdin).get('exp','none'))")
NOW=$(date +%s)
if [ "$EXP" != "none" ] && [ "$EXP" -lt "$NOW" ]; then
echo "Token is EXPIRED"
else
echo "Token is valid (expires: $(date -r $EXP))"
fi Common Mistakes with JWTs
- Storing in localStorage โ Accessible to any JavaScript on the page, including XSS payloads. Prefer
HttpOnlycookies for auth tokens. - No expiry โ Tokens without an
expclaim are valid forever. Always set a reasonable expiry and use refresh tokens for long sessions. - Sensitive data in payload โ Payloads are readable by anyone. Use opaque tokens (random IDs) or encrypt the payload if confidentiality matters.
- Not validating
audโ A token issued for service A can be replayed against service B ifaudis not checked. Always validate the audience claim.
Summary
A JWT is a signed JSON object encoded as Base64url. The header identifies the algorithm, the payload carries claims about the user or session, and the signature proves the token has not been modified. Decoding is trivial โ verifying requires the secret or public key. Treat JWTs as transparent containers: secure for integrity, but not private.