X Xerobit

JWT Token Structure — Header, Payload, and Signature Explained

A JWT has three Base64URL-encoded parts: header, payload, and signature, separated by dots. Here's how each part works, what claims mean, and how to read and decode a JWT token.

Mian Ali Khalid · · 4 min read
Use the tool
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
Open JWT Decoder →

A JWT (JSON Web Token) is three Base64URL-encoded JSON objects joined by dots: header.payload.signature. The header and payload are readable by anyone; the signature verifies authenticity.

Use the JWT Decoder to decode and inspect any JWT token.

JWT structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Split by .:

Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1: Header

Decoded:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg — signing algorithm: HS256, RS256, ES256, etc.
  • typ — type identifier, always "JWT"

Less common header fields:

  • kid — key ID (which signing key to use)
  • x5t — X.509 certificate thumbprint
  • jku — JWK Set URL

Part 2: Payload (Claims)

Decoded:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Standard registered claims (RFC 7519)

ClaimFull NameDescription
issIssuerWho issued the token ("auth.example.com")
subSubjectWho the token is about (user ID)
audAudienceWho should accept the token ("api.example.com")
expExpirationUnix timestamp when token expires
nbfNot BeforeUnix timestamp before which token is invalid
iatIssued AtUnix timestamp when token was issued
jtiJWT IDUnique identifier for the token

Full example payload

{
  "iss": "https://auth.example.com",
  "sub": "user_abc123",
  "aud": "https://api.example.com",
  "exp": 1748649600,
  "iat": 1748563200,
  "jti": "token_xyz789",
  "email": "alice@example.com",
  "roles": ["admin", "editor"],
  "plan": "pro"
}

Custom claims (email, roles, plan) are application-specific data.

Part 3: Signature

The signature verifies the token wasn’t tampered with:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

For HS256, the server signs with a secret key and verifies with the same key. For RS256, the server signs with a private key and anyone can verify with the public key.

Critical: The signature does NOT encrypt the payload. The payload is Base64URL-encoded (reversible), not encrypted. Anyone can decode and read a JWT. Never put sensitive data in the payload unless you encrypt the token (JWE).

Decoding in code

JavaScript

// Decode without verification (read claims):
function decodeJwt(token) {
  const [header, payload, signature] = token.split('.');
  
  const decode = (part) => {
    const padded = part.replace(/-/g, '+').replace(/_/g, '/');
    return JSON.parse(atob(padded));
  };
  
  return {
    header: decode(header),
    payload: decode(payload),
    signature,
  };
}

const { payload } = decodeJwt(token);
console.log(payload.sub, payload.exp);

// Verify and decode with jsonwebtoken:
import jwt from 'jsonwebtoken';

try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded.sub);  // user ID
} catch (err) {
  if (err.name === 'TokenExpiredError') console.log('Token expired');
  if (err.name === 'JsonWebTokenError') console.log('Invalid token');
}

Python

import base64
import json

def decode_jwt(token: str) -> dict:
    header_b64, payload_b64, signature = token.split('.')
    
    def decode_part(part):
        # Add padding if needed:
        padded = part + '=' * (4 - len(part) % 4)
        return json.loads(base64.urlsafe_b64decode(padded))
    
    return {
        'header': decode_part(header_b64),
        'payload': decode_part(payload_b64),
        'signature': signature,
    }

# With PyJWT (includes verification):
import jwt

try:
    payload = jwt.decode(token, secret, algorithms=['HS256'])
    print(payload['sub'])
except jwt.ExpiredSignatureError:
    print('Token expired')
except jwt.InvalidTokenError:
    print('Invalid token')

Checking expiration manually

function isTokenExpired(token) {
  const { payload } = decodeJwt(token);
  if (!payload.exp) return false;
  return Date.now() / 1000 > payload.exp;
}

function getExpiresIn(token) {
  const { payload } = decodeJwt(token);
  if (!payload.exp) return null;
  const secondsLeft = payload.exp - Date.now() / 1000;
  return Math.max(0, Math.floor(secondsLeft));
}

What Base64URL is

JWT uses Base64URL (not standard Base64) — it replaces + with - and / with _, and omits padding =. This makes tokens safe for use in URLs without encoding.

// Standard Base64:   ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
// Base64URL:         ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_

Related posts

Related tool

JWT Decoder

Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.

Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.