By AndyPublished
JWT Header, Payload and Signature Explained
The Three-Part Structure
HEADER.PAYLOAD.SIGNATURE eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTcxNjQ3NzM2MSwiZXhwIjoxNzE2NDgwOTYxfQ .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The first segment is the header. The second is the payload. The third is the signature. All three are Base64url-encoded. The header and payload are Base64url-encoded JSON objects. The signature is Base64url-encoded raw binary cryptographic output.
What is Base64url Encoding?
Base64url is a variant of standard Base64 designed to be safe in URLs, HTTP headers, and cookies. The differences from standard Base64:
- ·
+in standard Base64 becomes-in Base64url - ·
/in standard Base64 becomes_in Base64url - ·Padding characters (
=) are omitted
Encoding Walk-through: Step by Step
Here is exactly how a JWT header is produced from a plain JSON object. The same process applies to the payload.
// Input: plain JSON object
const header = { "alg": "HS256", "typ": "JWT" }
// Step 1: Serialize to a compact JSON string (no extra whitespace)
const json = JSON.stringify(header)
// → '{"alg":"HS256","typ":"JWT"}'
// Step 2: Encode to UTF-8 bytes
const bytes = new TextEncoder().encode(json)
// → Uint8Array [123, 34, 97, 108, 103, ...]
// Step 3: Standard Base64-encode the bytes
const b64 = btoa(String.fromCharCode(...bytes))
// → 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
// Step 4: Make URL-safe (replace + → -, / → _, strip =)
const b64url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
// → 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
// Decoding is the exact reverse:
// base64url → standard base64 → atob → TextDecoder → JSON.parseBecause Base64url is fully reversible with no key, pasting a JWT into any decoder — including this tool, immediately reveals the full header and payload in plain text. This is expected behaviour, not a vulnerability. The security comes from the signature, not the encoding.
The Header
The header is a JSON object that provides metadata about the token itself, primarily the signing algorithm and token type. A minimal header looks like:
{
"alg": "RS256",
"typ": "JWT"
}Common header parameters defined in RFC 7515 (JSON Web Signature):
- ·
alg: Algorithm. The cryptographic algorithm used to secure the token. Required. Common values:HS256,RS256,ES256,PS256. The valuenonemeans unsecured and must be rejected by any secure implementation. - ·
typ: Type. Identifies the media type of the token. Almost alwaysJWT. Optional; some issuers omit it. - ·
kid: Key ID. A hint identifying which key was used to sign the token. Useful when the issuer uses multiple keys (e.g., during key rotation). Receivers use thekidto select the correct public key from a JWKS endpoint. - ·
x5t: X.509 Certificate Thumbprint. A Base64url-encoded SHA-1 thumbprint of the DER-encoded X.509 certificate containing the signing key. Used in some enterprise PKI deployments. - ·
cty: Content Type. Used in nested JWTs to declare the MIME type of the inner content. Not common in plain JWTs.
Common Header Patterns from Real Identity Providers
Different platforms use different header conventions. Here are representative examples:
// Auth0 RS256 access token
{ "alg": "RS256", "typ": "JWT", "kid": "abc123def456" }
// AWS Cognito RS256 token
{ "kid": "1234example=", "alg": "RS256" }
// Google OAuth2 ID token
{ "alg": "RS256", "kid": "f05415b13acb9590f70df862765c655f5a7a019e", "typ": "JWT" }
// Firebase RS256 token
{ "alg": "RS256", "kid": "abc...def" }
// Internal HS256 service token (no kid, single secret)
{ "alg": "HS256", "typ": "JWT" }The presence of a kid in the header indicates the issuer uses multiple keys and publishes them at a JWKS endpoint. Always look up the matching key by kid before verifying, never assume you have the right key.
Algorithm Security Notes
The alg field has serious security implications. Never accept a token whose alg differs from what you expect. Two well-known attacks arise from failing to enforce the algorithm:
- ·Algorithm confusion (RS256 to HS256): Some broken implementations accept both symmetric and asymmetric tokens. An attacker who knows your RS256 public key can create a valid HS256 token signed with that public key as the HMAC secret.
- ·alg=none bypass: Implementations that blindly trust the header's
algfield can be tricked into accepting a token with"alg": "none", which has no signature at all.
The Payload
The payload is a JSON object containing the token's claims. A typical payload:
{
"iss": "https://auth.example.com/",
"sub": "user_a8f3c2d1",
"aud": "https://api.example.com",
"exp": 1716480961,
"nbf": 1716477361,
"iat": 1716477361,
"jti": "unique-token-id-8f3c2d",
"email": "jane@example.com",
"roles": ["admin", "editor"]
}The first seven claims are RFC 7519 registered claims. The last two (email and roles) are custom claims. All claims are optional by the spec, but exp, iss, and aud should be present and validated in production systems.
The payload is publicly readable. Never put passwords, private keys, credit card numbers, full SSNs, or unredacted health records in JWT claims. For sensitive data that must be included in a token, use JSON Web Encryption (JWE).
The Signature
The signature is what makes a JWT tamper-evident. It is computed over the UTF-8 encoding of the concatenated header and payload:
signing_input = base64url(UTF8(header)) + "." + base64url(UTF8(payload)) // For HS256: signature = HMAC-SHA256(signing_input, shared_secret) // For RS256: signature = RSA-PKCS1v15-SHA256(signing_input, private_key) // For ES256: signature = ECDSA-SHA256(signing_input, private_key) // The final token: token = signing_input + "." + base64url(signature)
What the Signature Covers
The signature covers both the header and the payload. If an attacker changes even a single character in either part, the signature will not match when recomputed. This makes the token tamper-evident.
What the Signature Does Not Cover
The signature does not encrypt the payload, it only authenticates it. It does not prevent someone from reading the claims. It does not validate the business logic of the claims (whether the expiry is reasonable, whether the issuer is trusted, etc.). Those checks are the responsibility of the verifying application.
Decoding vs Verifying
Decoding a JWT means Base64url-decoding the header and payload to reveal the JSON. Anyone can do this, no key is needed, because Base64url is not encryption.
Verifying a JWT means cryptographically confirming that the signature was produced by a specific key and that neither the header nor payload has been modified. This requires the correct key, either the HMAC shared secret or the issuer's public key.
To illustrate why this distinction matters, consider a tampered token. An attacker takes a legitimate JWT, changes "roles": ["user"] to "roles": ["admin"] in the payload, and re-encodes it. The new token decodes perfectly, the JSON is valid. But the signature no longer matches the modified payload. Any verifying service will reject it. An application that skips verification and uses the decoded claims directly is vulnerable to this exact attack.