A JSON Web Token looks like random gibberish until you understand its structure. It is actually three distinct pieces of data joined together — each with a specific job. This guide breaks down every part of a JWT token: what the header contains, what every payload claim means, how the signature is created and verified, and why every JWT token starts with the characters ey.

What is a JWT token?

A JSON Web Token (JWT) is a compact, self-contained string used to transmit verified information between two parties — typically a client and a server. It is defined in RFC 7519 and is the standard token format used in modern authentication systems including OAuth 2.0, OpenID Connect, Auth0, Firebase Authentication, and most REST APIs.

The key property of a JWT is that it is self-contained. The token carries all the information needed to identify the user and verify the token's authenticity. The server does not need to look up the token in a database — it only needs to verify the cryptographic signature. This makes JWTs stateless and highly scalable.

JWT is pronounced "jot" — as in a small note. The name comes from the initialism: JSON Web Token.

The three parts of a JWT token

Every JWT consists of exactly three parts separated by dots (.). Each part is independently Base64URL encoded:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzE2MjM5MDIyLCJleHAiOjE3MTYyNDI2MjJ9.hqWGSaFpvbrXkOWc6lrnpmdR2MBbmVkBLWwkGdG7Coo

  Header                    Payload                              Signature

The two dots are not part of the encoded data — they are fixed separators. A valid JWT always has exactly two dots and three non-empty segments. If you see a string that looks like a JWT but has a different number of dots, it is not a valid JWT.

Part 1 — the JWT header

The header is the first segment. It is a Base64URL-encoded JSON object that describes how the token was created. It contains two fields in almost every JWT:

{
  "alg": "HS256",  // signing algorithm
  "typ": "JWT"    // token type — always "JWT"
}

The alg field tells the server which cryptographic algorithm was used to create the signature. The most common values are:

AlgorithmTypeKey usedBest for
HS256Symmetric (HMAC)Shared secretSingle-server apps
HS384 / HS512Symmetric (HMAC)Shared secretHigher security HMAC
RS256Asymmetric (RSA)Private + public keyDistributed systems, microservices
ES256Asymmetric (ECDSA)Private + public keySmaller tokens, mobile apps
noneNoneNo keyNever use — dangerous

Never trust the algorithm specified in the token header. The server must always enforce which algorithms it accepts. Early JWT libraries that trusted the client-supplied alg field were vulnerable to the none algorithm attack — where an attacker set "alg": "none" to bypass signature verification entirely.

Part 2 — the JWT payload and claims

The payload is the second segment and the most important one. It is a Base64URL-encoded JSON object containing claims — key-value statements about the token and the entity it represents (usually a user).

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "[email protected]",
  "role": "admin",
  "permissions": ["read:users", "write:posts"],
  "iat": 1716239022,
  "exp": 1716242622,
  "iss": "https://api.example.com",
  "aud": "https://app.example.com",
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Registered claims

The JWT specification defines a set of registered claim names with standardised meanings. All of them are optional, but widely used:

ClaimNameTypePurpose
subSubjectStringIdentifies the principal — usually the user's ID
iatIssued AtUnix timestampWhen the token was created
expExpirationUnix timestampAfter this time the token is invalid
nbfNot BeforeUnix timestampToken is not valid before this time
issIssuerString / URIIdentifies who issued the token
audAudienceString / URIIdentifies who the token is intended for
jtiJWT IDString (UUID)Unique identifier for this specific token — used for revocation

Custom claims

Beyond the registered claims, you can add any key-value pair your application needs. Common custom claims include role, permissions, tenantId, orgId, and plan. These are sometimes called private claims when they are agreed between two specific parties, or public claims when registered in the IANA JSON Web Token Claims registry to avoid naming collisions.

The payload is not encrypted — only encoded. Base64URL encoding is reversible by anyone. Never put passwords, credit card numbers, social security numbers, or any sensitive personal data in a JWT payload. Only include information that is safe to expose to whoever holds the token.

Reading timestamps from the payload

The iat, exp, and nbf claims are all Unix timestamps — seconds elapsed since January 1, 1970 UTC. To convert to a human-readable date in JavaScript:

const payload = decoded.payload;

// Convert Unix timestamps to readable dates
const issuedAt  = new Date(payload.iat * 1000).toISOString();
const expiresAt = new Date(payload.exp * 1000).toISOString();

// Check if the token is currently valid
const now = Math.floor(Date.now() / 1000);
const isExpired  = payload.exp < now;
const notYetValid = payload.nbf > now; // if nbf claim exists

console.log(`Issued: ${issuedAt}`);
console.log(`Expires: ${expiresAt}`);
console.log(`Valid: ${!isExpired && !notYetValid}`);

Part 3 — the JWT signature

The signature is the third segment and the security foundation of the entire token. It is created by the server at the moment the token is issued, and it is what allows the server to verify that the token has not been tampered with.

For HS256, the signature is computed as:

// HS256 — symmetric, shared secret key
signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secretKey
)

// RS256 — asymmetric, private key signs
signature = RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

When the server receives a token on a subsequent request, it recalculates the expected signature using the same header, payload, and key. If the recalculated signature matches the one in the token, the token is authentic. If even a single byte of the header or payload was changed after the token was issued, the signatures will not match and the token is rejected.

The signature does not encrypt the payload — it only proves the payload has not changed since the token was issued. Think of it as a tamper-evident seal, not a lock.

HS256 vs RS256 — which to use

HS256 uses a single shared secret. Both the token issuer and every service that verifies tokens must hold the same secret. This is simple but creates a security problem in distributed systems: every service that can verify tokens can also create them.

RS256 uses a key pair. The private key signs tokens — only the auth server holds it. The public key verifies tokens — it can be distributed freely to any service that needs to verify tokens. This is the correct choice when multiple independent services need to verify tokens without being able to issue them.

// Node.js — issuing a token with RS256
const jwt = require('jsonwebtoken');
const fs  = require('fs');

const privateKey = fs.readFileSync('private.pem');
const publicKey  = fs.readFileSync('public.pem');

// Auth server: sign with private key
const token = jwt.sign(
  { sub: user.id, role: user.role },
  privateKey,
  { algorithm: 'RS256', expiresIn: '15m' }
);

// Any service: verify with public key only
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256']
});

Why do JWT tokens always start with "ey"?

This is one of the most common questions about JWTs and the answer is purely a consequence of Base64URL encoding.

Every JWT header and payload is a JSON object. JSON objects always start with an opening curly brace followed by a double quote: {". When you Base64URL encode those two characters, you always get ey:

// In JavaScript, btoa() encodes to standard Base64
btoa('{"')
// → "eyI="
//    ^^
//    always starts with "ey"

// Real JWT header example
btoa('{"alg":"HS256","typ":"JWT"}')
// → "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

// Real JWT payload example
btoa('{"sub":"123","role":"admin"}')
// → "eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ=="

Both the first and second parts of a JWT start with ey — because both are JSON objects starting with {". The third part (the signature) is raw binary data encoded as Base64URL, so it starts with whatever the cryptographic hash produces and has no predictable prefix.

This predictable ey…ey… pattern is useful in practice. You can detect accidentally logged or leaked JWTs in application logs, network traffic, or database fields by searching for the regex pattern:

// Regex to detect a JWT in logs or text
ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+

// In JavaScript
const jwtPattern = /ey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
const leaked = logLine.match(jwtPattern);

How JWT authentication works end to end

Understanding the structure of a token is more useful when you see how it moves through a real authentication flow:

  1. User submits credentials (email and password) to the login endpoint
  2. Server verifies credentials, creates a JWT with the user's sub, role, exp, and any other needed claims
  3. Server signs the JWT with its private key (RS256) or shared secret (HS256) and returns it
  4. Client stores the token and attaches it to every API request in the Authorization: Bearer <token> header
  5. Server extracts the token, verifies the signature and the exp claim, reads the payload claims, and processes the request
  6. When the token expires, the client uses a refresh token to obtain a new one — no re-login required
// Step 2-3: Server creates and signs a JWT
const accessToken = jwt.sign(
  {
    sub: user.id,
    role: user.role,
    jti: crypto.randomUUID()    // unique ID for revocation
  },
  process.env.JWT_SECRET,
  {
    algorithm: 'HS256',
    expiresIn: '15m',
    issuer: 'https://api.example.com',
    audience: 'https://app.example.com'
  }
);

// Step 4: Client sends the token
fetch('/api/profile', {
  headers: { 'Authorization': `Bearer ${accessToken}` }
});

// Step 5: Server verifies
const payload = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'],
  issuer: 'https://api.example.com',
  audience: 'https://app.example.com'
});

Common JWT structure mistakes

Putting sensitive data in the payload

The payload is Base64URL encoded — fully readable by anyone holding the token. Never include passwords, API keys, payment card data, social security numbers, or any regulated personal data. Keep payloads minimal: user ID, role, and expiry are usually enough.

Not setting an expiry claim

A JWT without an exp claim never expires. If it is ever stolen — from a log, a storage leak, or a network intercept — it remains valid forever. Always set exp. Short-lived access tokens (15 minutes) paired with a refresh token rotation pattern is the standard approach.

Using a weak or hardcoded secret key

For HS256, the entire security model rests on the secrecy of the signing key. A short or guessable key can be brute-forced offline using the token itself — no server access needed. Use a randomly generated key of at least 256 bits, store it in an environment variable, and never commit it to source control.

Trusting the algorithm from the token header

The alg field in the JWT header is supplied by the client. Never use it to decide how to verify the token. Always specify the expected algorithm explicitly on the server side.

Frequently asked questions

What is the structure of a JWT token?

A JWT token has three parts separated by dots: the header, the payload, and the signature. The header is a Base64URL-encoded JSON object describing the token type and signing algorithm. The payload is a Base64URL-encoded JSON object containing claims about the user and the token. The signature is a cryptographic hash of the header and payload that verifies the token has not been tampered with.

What are the three parts of a JWT token?

The three parts are the header, the payload, and the signature. The header contains alg (the signing algorithm) and typ (always "JWT"). The payload contains claims — including registered claims like sub, exp, iat, iss, and aud, plus any custom claims your application needs. The signature is a Base64URL-encoded cryptographic hash created by signing the encoded header and payload with a secret or private key.

What are JWT tokens and how do they work?

A JWT is a self-contained token that carries verified information about a user. When a user logs in, the server creates a JWT containing the user's ID, role, and expiry time, signs it cryptographically, and returns it. The client stores the token and sends it with every API request. The server verifies the signature on each request to confirm authenticity without a database lookup — making JWTs stateless and scalable across distributed systems.

Why do JWT tokens always start with EY?

Because both the header and payload are JSON objects that begin with the characters {". When Base64URL encoded, the two-character sequence {" always produces ey as the first two characters. Since every JSON object must start with a curly brace and every JWT part must be valid JSON, the ey prefix is a guaranteed mechanical consequence of the encoding — not a design choice.

What is the difference between HS256 and RS256?

HS256 uses a single shared secret key for both signing and verifying tokens. RS256 uses a key pair: a private key for signing (held only by the auth server) and a public key for verification (freely distributable). RS256 is the better choice for distributed systems and microservices because services can verify tokens without being able to create them.

Can you read a JWT payload without the secret key?

Yes. The payload is Base64URL encoded — not encrypted. Anyone with the token string can decode and read the claims without any key. The secret or private key is only needed to verify the cryptographic signature. This is why sensitive data should never be placed in a JWT payload.

Inspect any JWT instantly

Paste a token and see the decoded header, payload claims, and expiry time in a readable format. Runs entirely in your browser — nothing is sent to a server.

Open JWT Decoder →