By AndyPublished

JWT Header, Payload and Signature Explained

A JSON Web Token has exactly three parts. Each is a Base64url-encoded string, and the parts are joined with dots to form the compact token format. Understanding what each part contains, how it is encoded, and what it proves, or does not prove, is the foundation of working correctly with JWTs.

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
!
Base64url is an encoding, not encryption. It is trivially reversible , anyone can decode a Base64url string without a key. JWT payloads are therefore readable by anyone who has the token. Never store secrets in JWT claims.

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.parse

Because 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 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 value none means unsecured and must be rejected by any secure implementation.
  • ·typ: Type. Identifies the media type of the token. Almost always JWT. 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 the kid to 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 alg field 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.

This tool decodes JWTs immediately without a key. For signature verification, use the Verify tab and supply the appropriate key. Verification runs locally via the browser's Web Crypto API, your key is never transmitted.
Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder