JSONify Tools

Privacy-first JSON tools for developers

tools
  • Formatter
  • Minifier
  • Tree Viewer
  • Text Diff
more
  • JSON to CSV
  • Schema Validator
  • Path Finder
  • Key Sorter
  • Field Renamer
  • JSON/YAML
  • Base64
  • URL Encode
  • JWT Decoder
  • JSON Diff
  • Regex Tester
  • Hash Gen
  • Color Convert
  • Timestamp
links
  • Blog
  • FAQ
© 2026 JSONify ToolsAll processing happens in your browserPrivacy · Terms
Skip to main content
JSONify ToolsJSONify Toolsv2.0
HomeJSON FormatterJSON BeautifierJSON MinifierText CompareBlog
February 25, 2025
8 min read
Tutorial

What's Actually Inside a JWT Token

JWTs are everywhere in modern auth. Most developers use them without ever looking at what is inside. Here is what each part does, and why it matters for security.

JWTAuthenticationSecurityBase64

The Three Parts

A JWT is three base64url-encoded strings separated by dots. That is it. No magic, no binary protocol. Just three chunks of text.

A real JWT token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIEpvaG5zb24iLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzA5MTIzMjAwLCJleHAiOjE3MDkxMjY4MDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.4p8P1FGcaNaFxGAh7a8DcaEE_8GgT0vLspd82NYLxaM

Split on the dots and you get three parts:

Part 1 (Header): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Part 2 (Payload): eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIEpvaG5zb24iLC...

Part 3 (Signature): 4p8P1FGcaNaFxGAh7a8DcaEE_8GgT0vLspd82NYLxaM

Base64url Is Not Encryption

This is the most important thing to understand about JWTs. Base64url is an encoding, not encryption. It is reversible by anyone. No key required. No secret needed. Anyone who has the token can read the header and payload.

Decoding the header and payload in JavaScript:

function decodeJwtPart(part: string): object {
  // base64url -> base64 (replace URL-safe chars)
  const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
  // Decode and parse
  const json = atob(base64);
  return JSON.parse(json);
}

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIEpvaG5zb24iLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzA5MTIzMjAwLCJleHAiOjE3MDkxMjY4MDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.4p8P1FGcaNaFxGAh7a8DcaEE_8GgT0vLspd82NYLxaM";

const [header, payload] = token.split(".");

console.log(decodeJwtPart(header));
// { "alg": "HS256", "typ": "JWT" }

console.log(decodeJwtPart(payload));
// {
//   "sub": "1234567890",
//   "name": "Alice Johnson",
//   "email": "alice@example.com",
//   "role": "admin",
//   "iat": 1709123200,
//   "exp": 1709126800,
//   "iss": "auth.example.com",
//   "aud": "api.example.com"
// }

That is why you should never put sensitive data in a JWT payload. No passwords, no credit card numbers, no SSNs. Anyone who intercepts the token (from browser DevTools, server logs, or a proxy) can read everything in it. The signature only proves the token was not tampered with. It does not hide the contents.

The Header

The header is usually boring. It tells the server which algorithm to use for verifying the signature.

Typical header:

{
  "alg": "HS256",    // HMAC with SHA-256
  "typ": "JWT"       // Token type
}

Common algorithms you will see:

  • HS256: Symmetric. Same secret key for signing and verifying. Simple, good for single-server setups.
  • RS256: Asymmetric. Private key signs, public key verifies. Use this when multiple services need to verify tokens but only one service should issue them.
  • ES256: Asymmetric with elliptic curves. Smaller signatures than RS256, same security properties.
  • none: No signature. Never accept this in production. It has been the source of real CVEs where attackers set alg: "none" to bypass signature verification entirely.

The Payload: Standard Claims

The payload is where the actual data lives. JWT defines a set of standard claim names (all optional), and you can add any custom claims you need.

Standard (registered) claims:

{
  // "iss" (Issuer): who created this token
  "iss": "auth.example.com",

  // "sub" (Subject): who/what this token is about
  // Usually a user ID
  "sub": "user_abc123",

  // "aud" (Audience): who this token is intended for
  // Your API should reject tokens with the wrong audience
  "aud": "api.example.com",

  // "exp" (Expiration): Unix timestamp when token expires
  // After this time, the token should be rejected
  "exp": 1709126800,

  // "iat" (Issued At): when this token was created
  "iat": 1709123200,

  // "nbf" (Not Before): token is not valid before this time
  // Useful for tokens that should activate in the future
  "nbf": 1709123200,

  // "jti" (JWT ID): unique identifier for this specific token
  // Useful for revocation (store revoked JTI values in a blocklist)
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}

The three-letter names look cryptic, but they are intentionally short. JWTs travel in HTTP headers on every request, so keeping them compact matters. A few extra bytes per claim adds up at scale.

How Expiration Works

The exp claim is a Unix timestamp. When your server receives a JWT, it compares exp against the current time. If the token has expired, it gets rejected. No database lookup needed.

Checking expiration:

function isTokenExpired(token: string): boolean {
  const [, payloadB64] = token.split(".");
  const payload = JSON.parse(
    atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"))
  );

  if (!payload.exp) {
    // No expiration claim. Treat as expired (be strict).
    return true;
  }

  // exp is in seconds, Date.now() is in milliseconds
  const nowSeconds = Math.floor(Date.now() / 1000);

  // Allow 30 seconds of clock skew between servers
  const CLOCK_SKEW_SECONDS = 30;

  return payload.exp < (nowSeconds - CLOCK_SKEW_SECONDS);
}

// Common token lifetimes:
// Access tokens: 15 minutes to 1 hour
// Refresh tokens: 7 to 30 days
// Short-lived access tokens limit the damage if a token is stolen

The clock skew tolerance is important. If your auth server and your API server have clocks that are a few seconds apart (common in distributed systems), a token might appear expired on one server but valid on another. A 30-second buffer prevents spurious rejections.

JWS vs JWE: Signed vs Encrypted

Most JWTs you encounter are technically JWS (JSON Web Signature) tokens. The payload is readable by anyone; the signature just proves it was not modified. This is sufficient for most authentication use cases: the server needs to verify the token is genuine, and the payload contains non-sensitive data like user IDs and roles.

JWE (JSON Web Encryption) tokens are different. The payload is actually encrypted, so only the intended recipient with the decryption key can read the contents. JWE tokens have five parts separated by dots instead of three.

When to use which:

// JWS (what most people mean by "JWT")
// Structure: header.payload.signature
// Use when: payload is not sensitive
// Examples: user ID, role, permissions, email
// Anyone can READ the payload
// Nobody can MODIFY the payload without the key

// JWE (encrypted JWT)
// Structure: header.encryptedKey.iv.ciphertext.tag
// Use when: payload contains sensitive data
// Examples: PII, session data you want to hide from
//           browser DevTools or proxy inspection
// Nobody can READ or MODIFY the payload without the key

In practice, most applications use JWS and simply avoid putting sensitive data in the payload. If you find yourself wanting JWE because you need to hide the token contents from the client, consider whether a server-side session with an opaque session ID would be simpler.

Security Checklist

If you are implementing JWT auth, verify these things:

  • Always validate the signature. Do not decode the payload and trust it without checking the signature first.
  • Always check exp. Tokens without expiration live forever if stolen.
  • Validate iss and aud. A token from your staging auth server should not be accepted by your production API.
  • Reject alg: "none". Your JWT library should do this by default, but verify.
  • Use short lifetimes for access tokens. 15 minutes is a good default. Use refresh tokens for longer sessions.
  • Store tokens securely in the browser. HttpOnly cookies are safer than localStorage for access tokens. localStorage is readable by any JavaScript on the page, including XSS payloads.
  • Have a revocation strategy. JWTs are stateless by design, which means you cannot invalidate one without extra infrastructure. A short-lived token plus a blocklist of revoked jti values is the common approach.

Decode Your JWT Tokens

Paste any JWT to inspect the header, payload, and claims. All decoding happens in your browser.