Decoding JWTs: What's Actually Inside That Token
A JWT arrives in your authorization header, gets passed around between services, and carries claims about who the user is and what they're allowed to do. But what is it, exactly? And why can you read it without any key, if it's supposed to be a security mechanism?
The structure of a JWT
A JSON Web Token is three Base64url-encoded strings separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts: header, payload, signature. Each is independently decodable.
The header
Decode the first segment and you get a JSON object like this:
{
"alg": "HS256",
"typ": "JWT"
}
alg specifies the signing algorithm. Common values:
- HS256 — HMAC-SHA256. A shared secret is used for both signing and verification. The same key that creates the signature must be present to verify it.
- RS256 — RSA-SHA256. Asymmetric. A private key signs the token; a public key verifies it. This is common in OAuth2/OIDC systems where multiple services need to verify tokens but only one should issue them.
- ES256 — ECDSA-SHA256. Elliptic curve version of asymmetric signing. Shorter signatures than RS256 with equivalent security.
The algorithm matters — HS256 uses HMAC-SHA256, which is fast and secure for this purpose. If you want to understand the underlying hash functions and why algorithm choice matters, see MD5 vs SHA-256: Which Hash to Use.
The "alg: none" vulnerability. Some early JWT libraries accepted tokens with "alg": "none" and no signature, treating them as valid. This is a critical security flaw — an attacker could strip the signature and forge any claims. Any competent JWT library now rejects alg: none by default, but it's worth knowing this attack exists if you're auditing older systems.
The payload
The payload contains the claims — statements about the subject and other metadata:
{
"sub": "1234567890",
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"iat": 1716239022,
"exp": 1716242622,
"iss": "https://auth.example.com"
}
Standard registered claims (defined in RFC 7519):
sub(subject) — Who the token is about. Usually a user ID.iss(issuer) — Who issued the token. Usually the auth server URL.aud(audience) — Who the token is intended for. A service should reject tokens not addressed to it.exp(expiration time) — Unix timestamp after which the token is invalid. Always validate this.nbf(not before) — Unix timestamp before which the token should not be accepted.iat(issued at) — When the token was created. Useful for calculating token age.jti(JWT ID) — A unique identifier for the token. Can be used to prevent replay attacks if you maintain a list of used IDs.
Everything after those standard fields is a custom claim added by the application.
The signature
The signature is computed over the encoded header and payload:
HMACSHA256( base64url(header) + "." + base64url(payload), secret )
The signature allows a server to verify that the token hasn't been modified. If an attacker changes the payload (say, changing "role": "user" to "role": "admin"), the signature will no longer match, and a properly implemented server will reject the token.
What the signature does NOT do: provide confidentiality. The payload is not encrypted. Anyone who has the token can read every claim in it. The signature only proves the token hasn't been tampered with.
What you can verify client-side and what you cannot
Without the signing key, you can:
- Read any claim in the header or payload
- Check the
exptimestamp to see if the token has expired - Inspect the algorithm, issuer, subject, and any custom claims
Without the signing key, you cannot:
- Verify the signature (you'd need the secret key for HS256, or the public key for RS256/ES256)
- Trust that the claims haven't been modified (unverified tokens should not be used for authorization decisions)
In a browser context, JWT decoding tools — including ToolsKit's — are for inspection and debugging only. Your backend must verify the signature before trusting any claims.
What not to put in a JWT
Because the payload is readable by anyone with the token, sensitive information should not be included as plain claims:
- Passwords or password hashes
- Full credit card numbers or bank account details
- Private encryption keys
- Personally identifiable information beyond what's strictly necessary
Include only what the service needs to authorize a request: user ID, roles, permissions, and token metadata. If you need to carry sensitive data in a token, use JWE (JSON Web Encryption) instead of plain JWT — JWE encrypts the payload.
Token expiry and refresh flows
JWTs are typically stateless — the server doesn't store active tokens, it just validates them. This makes them efficient but creates a revocation problem: if a user logs out or their account is compromised, you can't "invalidate" a JWT that hasn't expired yet (unless you maintain a blocklist, which negates the statelessness advantage).
Short expiry times mitigate this. A common pattern: issue access tokens with 15-minute expiry and longer-lived refresh tokens. Access tokens are used for API requests; when they expire, the client uses the refresh token to get a new access token without re-authenticating.
JWT Decoder — Paste any JWT to instantly inspect the header, payload claims, expiry, algorithm, and issuer. Runs locally, nothing transmitted.
Open Tool