How to Decode and Inspect JWT Tokens

· 9 min read

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

  1. 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.
  2. 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.
  3. Check the claims: examine the expiration time, issuer, subject, audience, and any custom claims that drive your authorization logic.
  4. 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.
  5. Time-travel test: hover over iat, nbf, and exp to 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

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.