How to Decode and Inspect JWT Tokens
JSON Web Tokens (JWTs) are the most common way to handle authentication in modern web applications. When something goes wrong with auth, a user gets logged out unexpectedly, permissions are wrong, an API returns 401, decoding the JWT is usually the first debugging step. Understanding the three parts of a JWT, the standard claims, the algorithms it can be signed with, and the common pitfalls turns auth debugging from voodoo into a routine check.
A short history of JWT
JWTs were standardised in RFC 7519 in May 2015, after several years of draft iteration at the IETF. The format borrowed from earlier compact-token designs (SAML assertions, simple opaque cookies) but added two things they lacked: a strict JSON shape that was readable in any language, and a base64url-safe encoding that survived URL parameters, HTTP headers, and form fields without re-escaping. The companion specs, JWS (RFC 7515) for signatures, JWE (RFC 7516) for encryption, and JWA (RFC 7518) for algorithm names, together form the JOSE (JavaScript Object Signing and Encryption) family.
OAuth 2.0 and OpenID Connect adopted JWT as their default token format soon after, which is why almost every modern auth provider (Auth0, Okta, Cognito, Keycloak, Firebase, Supabase, Clerk) issues them today. The combination of self-contained tokens and stateless backends turned out to be a very natural fit for microservices and API gateways. The downside is that JWTs are notoriously easy to misuse, and the last decade has produced a steady stream of CVEs in libraries that did not validate the algorithm carefully.
What is inside a JWT
A JWT has three parts, separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
Header: contains the algorithm (HS256, RS256, etc.) and token type.
{"alg": "HS256", "typ": "JWT"}
Payload: contains claims (data assertions) about the user and token.
{"sub": "1234567890", "name": "Alice", "exp": 1700000000}
Signature, a cryptographic hash that verifies the token has not been tampered with. You cannot read this without the signing key.
Each section is base64url-encoded, which means it uses - and _ instead of + and / and omits the trailing = padding. Base64url is not encryption; pasting just the middle segment into any decoder reveals the payload. That is by design: middle segments are designed to be readable by services along the way, the signature is the only part that proves authenticity.
Common JWT claims
Standard claims are registered with IANA and defined in RFC 7519. Most are optional, but the ones below are nearly always present.
| Claim | Full name | What it contains |
|---|---|---|
sub |
Subject | User ID or identifier |
exp |
Expiration | Unix timestamp when token expires |
iat |
Issued At | Unix timestamp when token was created |
iss |
Issuer | Who created the token (your auth server) |
aud |
Audience | Who the token is intended for |
nbf |
Not Before | Token is not valid before this time |
jti |
JWT ID | Unique identifier for the token |
azp |
Authorized Party | The party the token was issued to (OIDC) |
scope / scp |
OAuth scopes | Permissions granted, often space-separated |
email |
Standard OIDC user identifier | |
name |
Name | Display name (OIDC) |
nonce |
Nonce | OIDC replay-protection value |
kid (header) |
Key ID | Which signing key was used (for JWKS lookup) |
Beyond the standard set, applications add their own custom claims (roles, tenant_id, feature_flags, permissions). Custom claim names are not namespaced by default, which means two different services can use the same name to mean different things; the OIDC convention of prefixing with a URI (https://myapp.com/roles) avoids the collision.
How to decode a JWT
- Paste your token: enter the complete JWT (header.payload.signature format) into the decoder. Browser-based decoders process it locally, the token never leaves the page.
- View the decoded sections: the tool displays the header (algorithm), payload (claims), and signature as formatted JSON, with timestamps shown as both Unix integers and human-readable dates.
- Check the claims: examine the expiration time, issuer, subject, audience, and any custom claims that drive your authorization logic.
- Compare to expectations: cross-reference the issuer against the auth provider you configured, the audience against the API the token is being sent to, and any role/scope claims against the permissions the user should have.
- Time-travel test: hover over
iat,nbf, andexpto see whether the token is currently valid, will soon expire, or was issued so long ago that your clock skew tolerance no longer covers it.
Signing algorithms
Not all JWTs use the same crypto. The alg header tells you which family the signature belongs to, and each has very different security properties.
| Algorithm | Family | Key type | When to choose |
|---|---|---|---|
HS256 |
HMAC | Shared secret | Single-service apps; never share the secret across teams |
HS384 / HS512 |
HMAC | Shared secret | Same as HS256 with longer digests |
RS256 |
RSA | Public/private keypair | Most common for OIDC; verifiers only need the public key |
RS384 / RS512 |
RSA | Keypair | Same as RS256 with larger keys |
PS256 / PS384 / PS512 |
RSA-PSS | Keypair | Modern RSA, recommended over RS for new deployments |
ES256 / ES384 / ES512 |
ECDSA | Elliptic-curve keypair | Smaller keys than RSA, faster verification |
EdDSA |
Ed25519 | Edwards-curve keypair | Newest, smallest, fastest; not yet universal |
none |
None | None | Forbidden in production; some old libraries still accept it |
Asymmetric algorithms (RS*, PS*, ES*, EdDSA) let any service verify a token with just a public key, which is why they dominate OIDC. Symmetric (HS*) is fine inside a single application but becomes a nightmare to rotate or distribute across multiple consumers.
Debugging with JWTs
Token expired? Check the exp claim. Convert the Unix timestamp to a human-readable date. If it is in the past, the token has expired and needs to be refreshed. Most JWT libraries reject expired tokens by default; if your app accepts them, that is a security bug.
Wrong permissions? Look for role or scope claims in the payload. These vary by implementation but often look like "role": "admin" or "scope": "read write profile".
User identity issues? The sub claim identifies the user. Verify it matches the expected user ID. Note that some providers use opaque GUIDs while others use email addresses; the decoder shows you exactly what is there.
Token not accepted? Check the aud (audience) claim. If the API expects a specific audience value and the token has a different one, it will be rejected. Audience mismatches are a common symptom of routing a token to the wrong service.
401 errors after deploy? Check the iss (issuer) claim. A new auth-provider tenant or a switched-out signing key changes the issuer URL; if your verifier still trusts the old one, every token looks invalid.
Clock skew problems? If iat is slightly in the future or exp is slightly in the past, your server's clock may be drifting. Most JWT libraries allow a few seconds of leeway; if not, an NTP-synchronised clock fixes the issue.
Common pitfalls
- **Trusting the
algheader without an allow-list, the classic JWT vulnerability was a server accepting whatever algorithm the token said it used. A token withalg: none(no signature) oralg: HS256(signed with your public key as the secret) could forge any payload. Pin the verifier to the exact expected algorithm. - **Putting secrets in the payload, the payload is base64url-encoded, not encrypted. Anyone with the token can read it. Never include passwords, API keys, or anything else you would not put in a query string.
- **Long-lived tokens without revocation, a 30-day JWT cannot be revoked without a token blacklist or session store. Keep access tokens short (5-60 minutes) and use a refresh-token flow for longer sessions.
- **Forgetting clock skew, servers in different time zones or with drifted clocks reject tokens that should be valid. Allow 30-60 seconds of leeway on
expandnbf. - **Trusting issuer without verification, the
issclaim is part of the payload that anyone can write. Verifying its value matches the configured issuer is required; allow-listing it prevents an attacker from minting tokens that claim to come from your provider. - **Reusing HS256 secrets across environments, the same secret in dev and prod means a leaked dev token works in production. Use per-environment keys, ideally fetched from a secret manager.
- **Storing tokens in localStorage, localStorage is readable by any JavaScript, so a single XSS bug exfiltrates every user's token. HttpOnly cookies with SameSite=Lax (or Strict) are the safer default.
- **Logging full tokens, application logs that capture full JWTs leak the tokens to anyone with log access. Truncate to the first 10 characters or log only the
jti. - **Ignoring
kidrotation, when a provider rotates signing keys, the new tokens have a newkidheader. A verifier that caches the JWKS forever will start rejecting valid tokens. Re-fetch the JWKS on key-id miss. - **Mixing JWT with sessions inconsistently, some endpoints behind JWT, others behind cookie sessions, leads to bugs where a logged-in user appears unauthenticated on one route. Pick one model per service.
Alternatives to JWT
JWT is dominant but not the only option. Each alternative trades different properties.
| Mechanism | Strength | Weakness |
|---|---|---|
| JWT (JWS) | Self-contained, easy across services | Cannot revoke without extra state |
| Opaque tokens + introspection | Easy to revoke, hides claims | Every request hits the auth server |
| Server-side sessions | Simplest model, instant revocation | Hard to scale across services |
| PASETO | Safer JWT replacement (no alg confusion) |
Smaller ecosystem |
| Macaroons | Built-in attenuation (delegated rights) | Limited library support |
| OAuth 2.0 + JWT access tokens | Industry standard for APIs | Spec is large, easy to mis-implement |
| OIDC ID tokens | Standard user identity + JWT | Often confused with access tokens |
| mTLS client certificates | Strongest auth at the transport layer | Cert management overhead |
For most teams the choice is between JWT and opaque tokens. JWT wins when verification needs to be cheap and offline; opaque tokens win when revocation has to be instant.
Privacy and the decoder
The JWT decoder runs entirely in your browser. The token you paste is split, base64url-decoded, and the JSON is parsed and pretty-printed without any network request. There is no log of the tokens that have been decoded, no analytics on the claims they contain, and no way for anyone to reconstruct who you were debugging for. JWTs often contain user identifiers, email addresses, internal role names, and tenant IDs, exactly the sort of metadata you do not want to send to a stranger's server. Decoding client-side keeps that information on your machine, which is the right default for any debugging task that touches authentication.
Frequently Asked Questions
Can I verify a JWT signature with a decoder?
No. Signature verification requires the signing secret or public key, which is kept on your server. A decoder shows you what is inside the token, but cryptographic verification must happen on your backend. Never trust an unverified JWT in production.
Is it safe to paste a JWT into an online tool?
Yes, when the tool runs in your browser. Browser-based decoders process the token locally, nothing is sent to a server. Avoid tools that make network requests with your token.
What is the exp claim?
The exp (expiration) claim is a Unix timestamp indicating when the token expires. After this time, the token should be rejected. Always check this claim when debugging authentication issues.
Can JWTs be encrypted?
Standard JWTs (JWS) are signed but not encrypted, anyone can decode the payload. JWE (JSON Web Encryption) tokens are encrypted, but they are less common. Never put sensitive data (passwords, secrets) in a standard JWT payload.
What is the alg none vulnerability?
Early JWT libraries accepted tokens with an alg header set to "none", meaning the signature could be omitted entirely. An attacker who set this header could forge any payload. Modern libraries reject "none" by default, but legacy systems may still be exposed; always allow-list the expected algorithm rather than trusting the header.
How should I store a JWT on the client?
HttpOnly secure cookies with SameSite=Lax (or Strict) are the safest default; they cannot be read by JavaScript, which mitigates XSS token theft. localStorage is convenient but vulnerable to any XSS bug. Never store long-lived JWTs alongside untrusted scripts.