How to Decode and Inspect JSON Web Tokens (JWT): Structure, Base64URL, and Security Traps
A security-focused developer guide on decoding JWTs client-side and server-side, parsing JOSE headers and payload claims, handling multi-byte UTF-8 strings, and avoiding verification anti-patterns.
JSON Web Tokens (RFC 7519) are compact, URL-safe tokens used widely for authentication and stateless session propagation. While inspecting a JWT is trivial once you understand its structure, treating decoded claims as trusted data without cryptographic verification is one of the most common security vulnerabilities in web architecture.
The Anatomy of a JSON Web Token Three Dot Separated Segments
A standard JWT is a single string composed of three distinct segments separated by periods (.):
| Segment | Purpose | Typical Contents | Security Function |
|---|---|---|---|
| 1. Header (JOSE) | Declares token metadata and signing parameters | {"alg": "RS256", "typ": "JWT", "kid": "key-01"} | Identifies which public key or algorithm must verify the signature |
| 2. Payload (Claims) | Contains statements about the subject and session | {"sub": "user_123", "role": "admin", "exp": 1735689600} | Data transport (unencrypted; readable by anyone who holds the token) |
| 3. Signature | Cryptographic proof of integrity | HMACSHA256(Base64(H) + '.' + Base64(P), Secret) | Prevents tampering with Header or Payload |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTc5ODc2MTYwMH0.u2P_f6fK4Z1g0X4PzH-4z7cK7p9n7Z6n5d8b7b2x3a4
|___________ HEADER ___________| |________________ PAYLOAD ________________| |_______________ SIGNATURE _______________|Architecture Comparison: Stateless JWTs vs. Opaque Session Tokens
Engineering teams must weigh critical trade-offs between stateless JWT bearer tokens and stateful opaque session IDs (stored in Redis or relational databases):
| Design Attribute | Stateless JWT Bearer Token | Opaque Session Token (Redis / DB) |
|---|---|---|
| Database Load on API Requests | Zero: Every microservice verifies signature locally without DB lookup | High: Every request queries central cache/DB to validate session |
| Instant Revocation (Logout/Ban) | Difficult: Token remains valid until 'exp' unless maintaining a blacklist | Instant: Deleting the session record immediately invalidates access |
| Header / Network Overhead | Heavy: 500 to 2,048 bytes transmitted on every HTTP request header | Lightweight: 32 to 64 bytes (random hex or UUID session cookie) |
| Payload Tamper Resistance | Cryptographic signature guarantees payload integrity | Server holds session state; client only holds an opaque pointer |
Base64URL Encoding Why Standard atob Fails
JWT segments are not encoded using standard Base64 (RFC 4648 §4); they use Base64URL (RFC 4648 §5). Base64URL replaces '+' with '-', replaces '/' with '_', and strips all trailing '=' padding characters so the string can be safely passed in URL query strings and HTTP headers. To explore how binary padding functions, see our technical primer on What is Base64 Encoding.
If you pass a raw JWT segment to browser atob() or Node.js basic decoders without restoring padding and characters, it will throw an encoding exception or corrupt multi-byte UTF-8 unicode characters (such as accented letters or emojis in user names).
Zero Dependency JavaScript JWT Decoder
Here is a production-safe, client-side decoder function that handles Base64URL character replacements, re-applies standard 4-byte boundary padding, and safely decodes multi-byte UTF-8 payloads:
function decodeJwt(token) {
if (typeof token !== "string") {
throw new TypeError("Token must be a string");
}
const parts = token.trim().split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format: must contain header, payload, and signature segments");
}
const decodeSegment = (segment) => {
// 1. Replace Base64URL characters with standard Base64
let base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
// 2. Pad string with '=' until length is a multiple of 4
while (base64.length % 4 !== 0) {
base64 += "=";
}
// 3. Decode Base64 and handle UTF-8 multi-byte characters
const binaryStr = atob(base64);
const bytes = Uint8Array.from(binaryStr, (char) => char.charCodeAt(0));
const jsonStr = new TextDecoder("utf-8").decode(bytes);
return JSON.parse(jsonStr);
};
return {
header: decodeSegment(parts[0]),
payload: decodeSegment(parts[1]),
signature: parts[2],
};
}
// Example usage:
const sampleJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg5MCIsIm5hbWUiOiJBbGV4IFJpdmVyYSIsImV4cCI6MTc5ODc2MTYwMH0.abc";
console.log(decodeJwt(sampleJwt).payload);
// -> { sub: "user_890", name: "Alex Rivera", exp: 1798761600 }Auditing Standard Claims exp nbf iat iss aud
The payload contains claims defined by RFC 7519 (detailed further in our JWT Structure and Claims Guide). When inspecting tokens during debugging or UI session routing, check these standard temporal claims:
- exp (Expiration Time): Unix epoch in seconds. If Date.now() >= exp * 1000, the token is expired and must be rejected.
- nbf (Not Before): Unix epoch in seconds. The token must not be accepted prior to this timestamp.
- iat (Issued At): Unix epoch in seconds indicating when the authentication authority minted the token.
- iss (Issuer): String identifying the identity provider (e.g. https://auth.company.com).
- aud (Audience): String or array of strings identifying the intended recipients or services.
Common Gotchas: Clock Skew Leeway & Sensitive Data Exposure
Two major operational gotchas occur when working with JWT claims in production:
- Clock Skew Rejections: In distributed cloud clusters, the authentication server minting the token and the API gateway validating it may have clocks that differ by 1 to 5 seconds. If a token is issued with nbf set to exact current time, the API gateway may reject it as 'not yet valid'. Solution: Always configure a 30 to 60 second clock skew tolerance (leeway) in validation libraries.
- Exposing Secrets in Payloads: Because JWT payloads are merely Base64URL-encoded (not encrypted), never store database passwords, API secret keys, or sensitive PII (Social Security numbers, medical records) in claims. Anyone inspecting browser LocalStorage or network proxies can read the entire payload.
Security Critical Decoding Is Not Verification
Decoding a JWT merely extracts JSON plaintext. Anyone with a text editor can modify {"role": "user"} to {"role": "admin"}, Base64URL-encode it, and send it to your backend.
Never trust a decoded JWT payload in authorization logic without verifying the cryptographic signature against your public key (for RS256/ES256) or shared secret (for HS256). Ensure your verification library rejects tokens where the alg header is modified to none or mismatched against expected algorithms.
[NEED: CoShareX JWT decoder privacy implementation detail — e.g. state that decoding runs 100% locally in browser memory with zero telemetry or network calls, making it safe for production bearer tokens and access credentials].
Code Formatter & Converter Suite
Validate, format, minify, and convert JSON, SQL, YAML, XML, and code dialects directly on your local machine.
Frequently Asked Questions
Can anyone read the information inside a JWT without the secret key?
Yes. The header and payload of a standard JWT are only Base64URL encoded, not encrypted. Any user, network proxy, or client who possesses the token can decode and view its claims. Never store sensitive credentials (like database passwords or API keys) in a JWT payload.
What is the difference between decoding a JWT and verifying a JWT?
Decoding converts the Base64URL strings back into readable JSON so you can inspect claims. Verifying recalculates the cryptographic signature using a secret or public key to mathematically confirm the token was issued by an authentic authority and was not altered in transit.
How do I check if a JWT has expired in JavaScript?
Decode the payload, extract the exp claim (which is in Unix seconds), multiply by 1000 to get milliseconds, and compare against Date.now(): const isExpired = Date.now() >= payload.exp * 1000;.
Conclusion
Client-side JWT decoding is a vital tool for session state and UI routing, but all privileged backend operations must validate the signature against a trusted cryptographic key.
Related Articles
JWT Structure & Claims: Anatomy of a JSON Web Token (RFC 7519)
Deep dive into JSON Web Token (JWT) architecture: RFC 7519 structure, registered vs custom claims, Base64URL encoding, and signature verification mechanics.
ConvertersWhat is Base64? Binary-to-Text Encoding Explained from Bitwise Roots
A deep architectural guide to Base64 encoding: 6-bit binary chunking, ASCII translation tables, padding mathematics, and URL-safe variants.