Skip to main content
CodeLint.Dev Dev Tools

JWT Decoder — Read Header & Payload

Decode a JSON Web Token and read every claim, with expiry timestamps rendered as dates. The token is never transmitted — it is decoded in your browser.

Payload (JSON)
Secret key
Output

Output will appear here

The registered claims

RFC 7519 reserves these seven names. All are optional, but they mean specific things and libraries act on them.

ClaimNameMeaning
issIssuerWho created the token. Verify it — a valid signature from the wrong issuer is still wrong
subSubjectWho the token is about, typically a user ID
audAudienceWho the token is for. A token minted for service A must be rejected by service B
expExpirationUnix seconds after which the token must be rejected
nbfNot beforeUnix seconds before which the token must be rejected
iatIssued atUnix seconds when the token was created
jtiJWT IDUnique identifier, used to detect replay or support revocation lists

exp, nbf and iat are in seconds, not milliseconds. Passing a JavaScript Date.now() value directly produces a token that expires in the year 56,000 — a real and frequent bug.

JWT implementation mistakes that become vulnerabilities

alg: none accepted

Cause:The specification permits an "unsecured" JWT with alg set to none and an empty signature. A verifier that reads the algorithm from the token itself will accept a forged token with any payload.

Fix:Never let the token choose its own verification algorithm. Configure the expected algorithm server-side and reject anything else. Most modern libraries do this by default; older ones did not, and this produced a wave of CVEs.

RS256 token verified with HS256

Cause:An attacker takes your public RSA key — which is public — and uses it as an HMAC secret to sign a forged token with alg changed to HS256. A verifier that trusts the header algorithm validates it successfully.

Fix:Same root cause as above: pin the algorithm server-side. This is why "just read alg from the header" is never acceptable.

Signature valid but token was for a different service

Cause:The aud claim is not being checked. Any token your identity provider signed is accepted everywhere.

Fix:Verify aud against this service’s own identifier, and iss against the expected issuer. Signature validity alone is not authorisation.

Revoked user still has access

Cause:JWTs are stateless by design — nothing consults a database at verification time, so a token remains valid until it expires no matter what happens to the account.

Fix:Keep access tokens short-lived (5–15 minutes) and pair them with a refresh token that is checked against server state. For immediate revocation you need a deny-list keyed on jti, which reintroduces the state JWTs were meant to avoid — worth doing consciously rather than by accident.

kid parameter used to load a key by path

Cause:The kid header names which key to verify with. Treating it as a filename or a SQL value makes it a path-traversal or injection vector controlled entirely by the attacker.

Fix:Look kid up in a fixed allow-list of known key identifiers. Never interpolate it into a path or a query.

The three parts

A JWT is three Base64url segments joined by dots: header.payload.signature. Decoding the first two needs no key at all.

Decoding a payload — no library needed
// Base64url differs from Base64: - for +, _ for /, no padding
function decodeSegment(segment) {
  const b64 = segment.replace(/-/g, '+').replace(/_/g, '/');
  const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, '=');
  return JSON.parse(atob(padded));
}

const [header, payload] = token.split('.').map(decodeSegment);

// This is decoding, NOT verification. It proves nothing about
// the token's authenticity. Never make an authorisation
// decision from a decoded-but-unverified payload.
Verifying properly, server-side
import jwt from 'jsonwebtoken';

const claims = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],          // pinned — never read from the token
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
  clockTolerance: 5               // seconds, for minor clock skew
});
Python
import jwt   # PyJWT

claims = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],          # pinned
    issuer="https://auth.example.com",
    audience="https://api.example.com",
)

About

The JWT Tool decodes any JSON Web Token to reveal its header (algorithm, type) and payload claims (subject, name, issued-at, expiry) without needing the secret — perfect for debugging auth flows. It automatically detects token expiry and flags expired tokens with a warning. In Encode mode, you can sign a JSON payload as a new JWT using HMAC-SHA-256, SHA-384, or SHA-512 and a custom secret key. All processing runs entirely in your browser using the Web Crypto API — your tokens, payloads, and secrets are never transmitted to any server. No sign-up required.

How to use

  1. 1 Paste your JWT string into the Decode tab to inspect the header and payload.
  2. 2 Switch to the Encode tab to create a new token.
  3. 3 Fill in the payload JSON and enter your signing secret.
  4. 4 Choose the algorithm (HS256, HS384, or HS512) and click Sign.
Can I decode a JWT without knowing the secret?
Yes. The header and payload of a JWT are Base64URL-encoded, not encrypted, so they can be decoded and read without the signing secret. Decoding does not verify the signature — it simply lets you inspect the claims. Never put sensitive data in a JWT payload.
Is it safe to paste my JWT into this tool?
All decoding and encoding runs entirely in your browser — your token is never sent to any server. That said, treat production tokens with care; use test or expired tokens when debugging in any online tool.
What signing algorithms are supported for encoding?
The encoder supports HS256 (HMAC-SHA-256), HS384 (HMAC-SHA-384), and HS512 (HMAC-SHA-512) — the most common symmetric signing algorithms. RSA and ECDSA (RS256, ES256) are asymmetric and require a private key, which is outside the scope of this browser-based tool.