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. It is 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:
| Algorithm | Type | Key used | Best for |
|---|---|---|---|
| HS256 | Symmetric (HMAC) | Shared secret | Single-server apps |
| HS384 / HS512 | Symmetric (HMAC) | Shared secret | Higher security HMAC |
| RS256 | Asymmetric (RSA) | Private + public key | Distributed systems, microservices |
| ES256 | Asymmetric (ECDSA) | Private + public key | Smaller tokens, mobile apps |
| none | None | No key | Never 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.
A third field, kid (Key ID), often appears in the header alongside alg and typ when a token is signed with RS256 or ES256. It tells the verifier which public key to use when the issuer has more than one key in rotation. More on this in the JWKS section below.
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": "john@example.com",
"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:
| Claim | Name | Type | Purpose |
|---|---|---|---|
| sub | Subject | String | Identifies the principal, usually the user's ID |
| iat | Issued At | Unix timestamp | When the token was created |
| exp | Expiration | Unix timestamp | After this time the token is invalid |
| nbf | Not Before | Unix timestamp | Token is not valid before this time |
| iss | Issuer | String / URI | Identifies who issued the token |
| aud | Audience | String / URI | Identifies who the token is intended for |
| jti | JWT ID | String (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. It is 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.
How big can a JWT get?
There is no hard size limit defined by the JWT spec itself, but practical limits come from HTTP infrastructure. Most servers, proxies, and load balancers cap total header size somewhere between 8 KB and 16 KB, and a JWT typically travels inside the Authorization header alongside other headers. Base64URL encoding also adds roughly 33% overhead on top of the raw JSON size.
Keep payloads small. A handful of short claims (user ID, role, expiry) usually produces a token well under 1 KB. Large claims such as full permission lists, group memberships, or profile data push a token toward several KB and slow down every request, since the full token is re-sent on every call. If you need to attach large amounts of user data, store it server-side and put only a reference, such as the user ID, in the payload.
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'] });
JWT, JWS, JWK, and JWKS: what is the difference?
These four acronyms belong to the same family of specifications and are easy to mix up:
- JWT (JSON Web Token) is the compact token format covered in this guide: three Base64URL-encoded parts joined by dots.
- JWS (JSON Web Signature) is the underlying specification, RFC 7515, that defines how to sign a payload and produce a compact string. A signed JWT is technically a JWS.
- JWK (JSON Web Key) is a single cryptographic key represented as a JSON object, used to describe a public or private key in a standard, language-independent format.
- JWKS (JSON Web Key Set) is a JSON document containing an array of JWKs, typically published at a well-known URL such as
/.well-known/jwks.json. Services that verify RS256 or ES256 tokens fetch the issuer's JWKS to get the current public key instead of hardcoding it.
// Example JWKS document { "keys": [ { "kid": "a1b2c3", "kty": "RSA", "use": "sig", "alg": "RS256", "n": "...", "e": "AQAB" } ] }
When an issuer publishes a JWKS, each JWT header includes a kid (Key ID) claim so the verifier knows which key in the set to use. This matters when keys are rotated periodically, since a verifier can fetch the new public key by its kid without invalidating tokens signed under the previous key while it is still in the set.
A complete JWT example, decoded step by step
Here is a full example token and what each part decodes to, using the same structure shown throughout this guide:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzE2MjM5MDIyLCJleHAiOjE3MTYyNDI2MjJ9.hqWGSaFpvbrXkOWc6lrnpmdR2MBbmVkBLWwkGdG7Coo
Decoding the first segment, the header, gives:
{ "alg": "HS256", "typ": "JWT" }Decoding the second segment, the payload, gives:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1716239022,
"exp": 1716242622
}Reading this payload: the token belongs to user 1234567890 (John Doe), grants the admin role, was issued at Unix time 1716239022, and expires at 1716242622, about one hour later.
The third segment, hqWGSaFpvbrXkOWc6lrnpmdR2MBbmVkBLWwkGdG7Coo, is the raw signature. It cannot be decoded into readable data. It can only be recalculated with the correct secret key and compared against this value to confirm the token is authentic.
Paste this token, or any token, into the JWT Decoder to see this same breakdown instantly, with the expiry converted to a readable date, entirely in your browser.
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:
- User submits credentials (email and password) to the login endpoint
- Server verifies credentials, creates a JWT with the user's
sub,role,exp, and any other needed claims - Server signs the JWT with its private key (RS256) or shared secret (HS256) and returns it
- Client stores the token and attaches it to every API request in the
Authorization: Bearer <token>header - Server extracts the token, verifies the signature and the
expclaim, reads the payload claims, and processes the request - 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' });
Sending a JWT in the Authorization header
Once a client has a JWT, it is almost always sent using the HTTP Authorization request header with the Bearer scheme, defined in RFC 6750:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The word Bearer is followed by exactly one space, then the full three-part token. There is no quoting and no trailing punctuation. A common mistake is sending just the token without the Bearer prefix, or copying an extra space or newline character along with the token, which breaks the signature check on the server.
To test an endpoint manually with a JWT, most HTTP clients accept the same format:
// curl curl -H "Authorization: Bearer eyJhbGciOi..." https://api.example.com/profile // Postman: Authorization tab, type "Bearer Token", paste the token value
Some APIs also accept a JWT as a query parameter or inside a cookie, but the Authorization header is the standard, and the one assumed by most JWT middleware libraries by default.
Decoding a JWT in other languages
The examples on this page use JavaScript, but decoding a JWT (reading the header and payload without checking the signature) works the same way in any language: split the token on the dots, then Base64URL-decode each part.
Python
# pip install pyjwt import jwt # decode() without a key just reads the claims payload = jwt.decode(token, options={"verify_signature": False}) print(payload["sub"], payload["exp"])
Java
// com.auth0:java-jwt DecodedJWT jwt = JWT.decode(token); String subject = jwt.getClaim("sub").asString(); Date expiresAt = jwt.getExpiresAt();
C#
// System.IdentityModel.Tokens.Jwt var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(token); var sub = jwt.Claims.First(c => c.Type == "sub").Value;
PHP
// firebase/php-jwt (decode without verifying) $parts = explode('.', $token); $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); echo $payload['sub'];
Every one of these snippets decodes the payload without verifying the signature. That is fine for reading claims during development or debugging, but never treat a decoded, unverified payload as trustworthy for an authorization decision in production code.
Does JWT structure differ across Auth0, Firebase, Cognito, Keycloak, or Azure AD?
No. Every identity provider that issues JWTs, including Auth0, Firebase Authentication, AWS Cognito, Keycloak, and Azure AD (Microsoft Entra ID), uses the exact same three-part header.payload.signature structure described on this page. OpenID Connect (OIDC) and OAuth 2.0 both use JWT as their standard token format. They do not define a different one.
What changes between providers is which claims appear in the payload, not the structure itself. A few provider-specific claims to expect:
| Provider | Notable custom claims | Typical algorithm |
|---|---|---|
| Auth0 | azp, custom namespaced claims | RS256 |
| Firebase Auth | user_id, firebase (provider info) | RS256 |
| AWS Cognito | cognito:username, token_use | RS256 |
| Keycloak | realm_access, resource_access | RS256 |
| Azure AD / Entra ID | tid (tenant ID), oid, roles | RS256 |
All five default to RS256 because they are distributed identity systems: the private signing key never leaves the provider, and any client application only needs the provider's public key, usually fetched from a JWKS endpoint, to verify tokens.
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.
Troubleshooting: why do I get an "invalid signature" error?
An invalid signature error means the server recalculated the expected signature and it did not match the one in the token. The header and payload can still decode fine, since decoding never checks the signature, but verification fails. Common causes:
- Wrong secret or key: the token was signed with a different secret, or a different environment's key, than the one the server is verifying against.
- Algorithm mismatch: the token was signed with RS256 but the server is verifying with an HS256 secret, or vice versa.
- Token was edited after signing: even changing a single character in the payload, such as manually editing a
roleclaim, invalidates the signature. This is by design: it is what makes the signature useful. - Whitespace or line breaks copied with the token: pasting a JWT from an email client or a document sometimes introduces extra characters that change the exact string being verified.
- Using the wrong key from a rotated JWKS: if the issuer rotates its signing keys, a verifier caching an old public key or missing the
kidlookup will fail to verify newly issued tokens. - Clock skew on
expornbf: technically a separate error from an invalid signature, but often reported together: the server's clock and the token's timestamps have drifted enough to reject an otherwise valid token.
To isolate the problem, decode the token first (no key needed) to confirm the header and payload look correct, then check that the exact same secret, key, and algorithm used to sign the token are being used to verify it.
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.
How many parts does a JWT have?
A JWT always has exactly three parts: the header, the payload, and the signature. They are joined by two dots in the format header.payload.signature. If a string has more or fewer than two dots, it is not a valid JWT.
What is the difference between a JWT, a JWS, and a JWK?
JWT (JSON Web Token) is the compact token format described on this page. JWS (JSON Web Signature) is the underlying specification that defines how the token is signed. A signed JWT is technically a JWS. A JWK (JSON Web Key) is a single cryptographic key represented as JSON, and a JWKS (JSON Web Key Set) is a collection of JWKs, usually published at a well-known URL so verifiers can fetch the current public keys.
Why am I getting an invalid signature error when verifying a JWT?
The most common causes are using the wrong secret or key, a mismatch between the algorithm used to sign the token and the one used to verify it, the token being edited after it was signed, or extra whitespace accidentally copied along with the token string. Decoding the token first confirms the header and payload are well-formed, then check that the exact same secret, key, and algorithm are used on both sides.
Do Auth0, Firebase, and Keycloak use a different JWT structure?
No. Every identity provider, including Auth0, Firebase Authentication, AWS Cognito, Keycloak, and Azure AD, issues tokens using the same three-part header.payload.signature structure. What differs is which custom claims appear in the payload and, for RS256 tokens, where the public key used to verify them is published.
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 →