If you have ever squinted at a 500-character dot-separated base64url string trying to figure out why an API request returned 401 Unauthorized, this guide is for you.
JSON Web Tokens (JWT, defined in RFC 7519) are the industry standard for stateless authentication and authorization. However, inspecting and debugging them safely requires understanding their internal structure and avoiding common security pitfalls.
Anatomy of a JSON Web Token#
A JWT consists of three distinct parts separated by periods (.):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWxpY2UiLCJpYXQiOjE3MTYyMzkwMDB9.4z9sK...
[------ HEADER (Base64Url) ------].[------ PAYLOAD (Base64Url) ------].[--- SIGNATURE ---]1. Header
The header specifies the token type and cryptographic algorithm used to generate the signature:
alg: The cryptographic algorithm (e.g.HS256for HMAC-SHA256,RS256for RSA-SHA256,ES256for ECDSA).typ: Token type, typically"JWT".kid(Optional): Key ID used to identify which public key signed the token in a JWKS key set.
2. Payload (Claims)
The payload contains the claims — statements about the entity (typically the user) and additional metadata:
iss(Issuer): Identifies the authorization server that issued the token.sub(Subject): Unique identifier for the authenticated user.aud(Audience): Intended recipient(s) for the token.exp(Expiration Time): Unix epoch timestamp after which the token is invalid.nbf(Not Before): Unix epoch timestamp before which the token must not be accepted.iat(Issued At): When the token was created.jti(JWT ID): Unique identifier for the token (used to prevent replay attacks).
3. Signature
The signature is created by taking the encoded header, encoded payload, and signing them using the private key (RS256) or secret key (HS256).
Browser-Native Safe JWT Decoding#
Decoding a JWT does not require sending it to an external server. You can decode it directly in your browser using standard JavaScript:
function parseJwtSafely(token: string) {
const parts = token.trim().split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format: Token must have 3 segments');
}
// Base64Url to standard Base64 conversion
const base64UrlToBase64 = (str: string) => {
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) base64 += '=';
return base64;
};
const decodeSegment = (segment: string) => {
const base64 = base64UrlToBase64(segment);
const binary = atob(base64);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const jsonStr = new TextDecoder('utf-8').decode(bytes);
return JSON.parse(jsonStr);
};
const header = decodeSegment(parts[0]);
const payload = decodeSegment(parts[1]);
return { header, payload, rawSignature: parts[2] };
}Common Vulnerabilities and Traps#
- 1Algorithm Confusion (
"alg": "none"): Early insecure implementations accepted tokens with"alg": "none"without validating signatures. Modern verifiers must explicitly enforce allowed algorithms. - 2Public vs Symmetric Confusion (RS256 vs HS256): If a backend expects an RSA public key (RS256) but an attacker signs with HMAC (HS256) using the public key as the symmetric secret, vulnerable verifiers may pass the token.
- 3Clock Skew: Token validation should account for a small clock drift allowance (typically 30–60 seconds) when comparing
expandnbf. - 4Token Storage:
localStoragevshttpOnlyCookies: Storing access tokens inlocalStorageexposes them to Cross-Site Scripting (XSS). Sensitive access tokens should reside inhttpOnly; Secure; SameSite=Strictcookies or short-lived memory state.
Rule of Thumb#
Decoding a JWT tells you what it claims. Verifying the cryptographic signature tells you whether to trust it. Always use client-side tools to inspect claims and server-side libraries to enforce signatures.