Why Decode Only the Header?
99% of JWT decoders show you both the header and the payload. But sometimes you only want the header — specifically when debugging key rotation,JWKS kid mismatches, or unexpected algorithmerrors. The payload might be sensitive (user emails, custom claims) that you shouldn't paste into a third-party decoder, while the header is almost always safe to inspect: alg, kid, typ. For auth providers like Auth0, Okta, Cognito, Firebase, and Azure AD, the header is where the debugging gold lives.
Header Claim Reference
// Typical modern JWT header
{
"alg": "RS256", // required — HS256|RS256|ES256|EdDSA|none
"typ": "JWT", // optional — usually "JWT"
"kid": "4f8d2c1a" // key ID in the issuer's JWKS
}
// Legacy / stripped-down header
{ "alg": "HS256" }
// Nested JWT (JWT wrapping another JWT)
{
"alg": "RS256",
"typ": "JWT",
"cty": "JWT" // content is another JWT
}
// Dangerous — alg: none (never accept)
{ "alg": "none" }
// Suspicious — jku points to attacker-controlled URL
{
"alg": "RS256",
"kid": "...",
"jku": "https://attacker.example.com/jwks.json" // treat as hostile
}Method 1: JavaScript (Browser + Node.js)
Vanilla (no dependencies)
function decodeHeader(token) {
const seg = token.split('.')[0];
if (!seg) throw new Error('Invalid JWT');
const b64 = seg.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(atob(b64));
}
const header = decodeHeader(token);
console.log(header.alg); // "RS256"
console.log(header.kid); // "4f8d2c1a"
console.log(header.typ); // "JWT"jose library
import { decodeProtectedHeader } from 'jose';
const header = decodeProtectedHeader(token);
// { alg: 'RS256', kid: '4f8d2c1a', typ: 'JWT' }jsonwebtoken library
const jwt = require('jsonwebtoken');
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg, kid, typ }Method 2: Python
Standard library (no deps)
import base64, json
def decode_header(token: str) -> dict:
seg = token.split('.')[0]
# Pad to multiple of 4
padded = seg + '=' * (-len(seg) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
header = decode_header(token)
print(header['alg']) # 'RS256'
print(header.get('kid'))python-jose
from jose import jwt
header = jwt.get_unverified_header(token)
# {'alg': 'RS256', 'kid': '4f8d2c1a', 'typ': 'JWT'}PyJWT
import jwt
header = jwt.get_unverified_header(token)
print(header['alg'])
print(header.get('kid'))Method 3: Bash / curl / jq
# One-liner — decode header from an environment variable
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null | jq .
# Add padding safely with python if base64 complains
echo "$TOKEN" | python3 -c "
import sys, base64, json
seg = sys.stdin.read().strip().split('.')[0]
pad = '=' * (-len(seg) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(seg + pad)), indent=2))
"Method 4: JWKS Lookup Using the Header kid
import { decodeProtectedHeader } from 'jose';
async function findJwksKey(token, jwksUrl) {
const { kid, alg } = decodeProtectedHeader(token);
const res = await fetch(jwksUrl);
const jwks = await res.json();
const key = jwks.keys.find(k => k.kid === kid);
if (!key) {
throw new Error(`No key with kid=${kid} in JWKS at ${jwksUrl}`);
}
if (!['RS256', 'RS384', 'RS512', 'ES256'].includes(alg)) {
throw new Error(`Unexpected alg=${alg}; refusing to verify`);
}
return { key, alg };
}Security — Always Validate alg Before Verifying
A classic JWT attack is algorithm confusion: an attacker takes a server's RSA public key, treats it as an HMAC-SHA256 secret, signs a forged token, and sets the header to alg: HS256. If the server verifies with whatever algorithm the header claims, the attack succeeds. The fix is to always pass an explicit algorithms allowlist to your verifier:
// Right
await jwtVerify(token, key, { algorithms: ['RS256'] });
// Wrong — library may fall back to header's alg
await jwtVerify(token, key); // never do thisCommon Header-Only Decode Pitfalls
- Decoding segment 1 by mistake — segment 0 is header, segment 1 is payload. Easy to swap.
- Base64 vs Base64URL — JWT uses Base64URL. Replace
-→+and_→/before standard decoders. - Missing padding in Python/Dart —
base64.urlsafe_b64decodeis strict. Pad with=to a multiple of 4. - Trusting the alg claim — the header is NOT signed in the sense that an attacker who knows your public key can forge any header. Always validate alg against your allowlist.
- Accepting
noneorjku— both are legitimate RFC values but highly dangerous in production. Modern libraries reject them by default. - Blindly fetching jku URL — if your verifier respects jku and the URL is attacker-controlled, you're pwned. Hardcode the JWKS URL instead.
Related Tools
- JWT Decoder Online — full header + payload UI
- JWT Parser Online — structural breakdown
- Verify JWT Signature — JWKS & key rotation guide
- JWT RS256 Decoder — RS256 deep dive
- JWT Expiration Checker — exp claim deep dive