← All articles

How to Decode a JWT Safely: Header, Payload, Claims, and Verification

Decode a JWT locally, read header and payload claims, handle Base64url and Unicode safely, and understand why decoding is not signature verification.

A JWT is easy to read and easy to misunderstand. It looks like an opaque credential, but a normal signed JWT is just three Base64url sections joined by dots:

header.payload.signature

The first two sections are JSON. Anyone with the token can decode them without a secret key. That is useful when you are debugging login, checking an exp timestamp, or confirming which aud a token was minted for. It is also why the payload is not a place for passwords, API keys, private profile data, or anything you would not show in a log.

The practical rule is simple: decode a JWT to inspect it; verify a JWT before trusting it.

Quick Answer

To decode a JWT:

  1. Remove an optional Bearer prefix.
  2. Split the token on dots.
  3. Base64url-decode the first section into the header JSON.
  4. Base64url-decode the second section into the payload JSON.
  5. Treat the third section as the signature bytes, not readable JSON.
  6. Read claims such as sub, iss, aud, exp, nbf, and iat.
  7. Verify the signature and required claims on the server before using the token for authorization.

No secret key is needed for step 1 through step 6. A key is needed only when you verify the signature.

What a JWT Looks Like

Here is a compact example token. It is useful for decoding practice, not a production credential:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMn0.X6sdNf_Fd-AFXKXrbTXHt3QuGJs6aTrwhYR6F4tZNIY

Split it at the dots:

Section Decodes to What it is for
eyJhbGci... Header JSON Algorithm, type, key hint
eyJzdWIi... Payload JSON Claims about the subject
X6sdNf... Signature bytes Tamper detection

Decoded header:

{
  "alg": "HS256",
  "typ": "JWT"
}

Decoded payload:

{
  "sub": "1234567890",
  "name": "Ada Lovelace",
  "iat": 1516239022
}

The signature does not become a nice JSON object. It is cryptographic output over the encoded header and encoded payload. A decoder can show that a signature section exists, but only verification can prove whether it is valid.

JWT, JWS, and JWE in Plain English

These names are close enough to cause confusion:

Term What it means Can you read the payload by decoding?
JWT JSON Web Token, a claims format defined by RFC 7519 Usually yes, when represented as a signed JWS
JWS JSON Web Signature, integrity protection for a readable payload Yes
JWE JSON Web Encryption, encrypted payload for confidentiality No, not without decryption

Most web app "JWTs" are compact JWS tokens with three sections. They are signed, not encrypted. A compact JWE normally has five sections. If your token has five dot-separated parts, a simple JWT decoder will not show the claims because the payload is encrypted.

How to Decode a JWT in the Browser

JWT uses Base64url, not plain Base64. Base64url replaces + with -, replaces / with _, and often omits = padding. A robust decoder puts those details back before decoding.

This version also handles Unicode safely by decoding bytes through TextDecoder, which is better than using atob() output as text directly.

function base64UrlToJson(part) {
  const base64 = part
    .replace(/-/g, '+')
    .replace(/_/g, '/');

  const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
  const binary = atob(padded);
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  const json = new TextDecoder().decode(bytes);

  return JSON.parse(json);
}

function decodeJwt(token) {
  const trimmed = token.trim().replace(/^Bearer\s+/i, '');
  const parts = trimmed.split('.');

  if (parts.length < 2 || !parts[0] || !parts[1]) {
    throw new Error('Not a JWT: expected header.payload.signature');
  }

  return {
    header: base64UrlToJson(parts[0]),
    payload: base64UrlToJson(parts[1]),
    signature: parts[2] || ''
  };
}

Use it for inspection:

const decoded = decodeJwt(token);

console.log(decoded.header.alg);
console.log(decoded.payload.sub);
console.log(decoded.payload.exp);

Do not use that decoded payload as proof of identity. The code above never checks whether the signature matches.

How to Decode a JWT in Node.js

Modern Node can decode Base64url directly with Buffer.

function decodePart(part) {
  return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
}

function decodeJwt(token) {
  const trimmed = token.trim().replace(/^Bearer\s+/i, '');
  const [headerPart, payloadPart, signature = ''] = trimmed.split('.');

  if (!headerPart || !payloadPart) {
    throw new Error('Not a JWT: expected header.payload.signature');
  }

  return {
    header: decodePart(headerPart),
    payload: decodePart(payloadPart),
    signature
  };
}

This is still only decoding. In production, use a JOSE/JWT library to verify the token and enforce iss, aud, exp, nbf, and allowed algorithms.

How to Decode a JWT in Python

Python's base64.urlsafe_b64decode() handles the URL-safe alphabet, but you still need to restore padding.

import base64
import json

def decode_part(part):
    padded = part + "=" * (-len(part) % 4)
    decoded = base64.urlsafe_b64decode(padded)
    return json.loads(decoded.decode("utf-8"))

def decode_jwt(token):
    token = token.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()

    parts = token.split(".")
    if len(parts) < 2 or not parts[0] or not parts[1]:
        raise ValueError("Not a JWT: expected header.payload.signature")

    return {
        "header": decode_part(parts[0]),
        "payload": decode_part(parts[1]),
        "signature": parts[2] if len(parts) > 2 else ""
    }

If the token came from a real authorization header, avoid printing it in full. Log a short prefix or a hash, not the entire credential.

Reading the Header

The header is small, but it tells you how the token expects to be verified.

Header field Example What to check
alg RS256 The signing algorithm. Do not trust it blindly; allow-list expected algorithms in your verifier.
typ JWT Usually informational. Some systems use it to distinguish token types.
kid 2026-07-key-1 Key ID. Use it only to look up a key from a trusted key set.
cty JWT Content type, sometimes used for nested tokens.

The two risky fields are alg and kid.

alg is dangerous when a server accepts whatever the token says. A forged token can claim alg: "none" or switch from an asymmetric algorithm to a symmetric one if the verifier is misconfigured. Good libraries let you pass an explicit allow-list such as ['RS256'].

kid is also attacker-controlled input. Treat it as a lookup hint inside a fixed JWKS, not as a file path, SQL value, or arbitrary URL.

Reading the Payload Claims

JWT payload claims are just JSON fields. Some are registered by the JWT standard; others are custom to your app.

Claim Meaning Common debugging question
iss Issuer Did this token come from the identity provider I trust?
sub Subject Which user, service account, or principal is this token about?
aud Audience Was this token intended for my API?
exp Expiration time Is the token already expired?
nbf Not before Is the token being used too early?
iat Issued at When was it minted?
jti JWT ID Can this token be tracked or revoked?

Time claims are Unix timestamps in seconds, not JavaScript milliseconds.

const expiresAt = new Date(payload.exp * 1000);

If you forget * 1000, your expiry date will look like it belongs near January 1970. If you multiply a value that was already milliseconds, it will jump far into the future. When debugging, inspect the raw number first.

Custom claims might look like this:

{
  "tenant_id": "acme",
  "role": "admin",
  "scope": "read:invoices write:invoices"
}

Those fields are application policy, not JWT magic. Your server still needs to decide whether the subject may actually perform the requested action.

Decoding Is Not Verifying

This is the mistake that creates security bugs: a decoded payload is not a trusted payload.

Anyone can create this JSON:

{
  "sub": "123",
  "role": "admin",
  "exp": 4102444800
}

Anyone can Base64url-encode it into a JWT-looking token. A decoder will show "role": "admin" just fine. Verification is what checks whether a trusted issuer actually signed that exact header and payload.

Use decoding for:

  • Debugging what a token says.
  • Checking whether a frontend received the expected claims.
  • Inspecting expiration and audience values.
  • Confirming whether a token is JWS-like or JWE-like.

Use verification for:

  • Login sessions.
  • API authorization.
  • Role or permission decisions.
  • Accepting tokens from another service.
  • Anything that changes data.

A Safe Verification Checklist

Verification belongs on the server or in a trusted backend service. The exact API depends on your JWT library, but the checklist is consistent:

  1. Choose the expected algorithms in code. Do not accept every alg value from the token.
  2. Select the verification key from trusted configuration or a trusted JWKS.
  3. Treat kid as an untrusted key hint, not a free-form path or URL.
  4. Verify the signature over the original encoded header and payload.
  5. Check exp and nbf, allowing only a small clock skew if needed.
  6. Check iss against the expected issuer.
  7. Check aud against your API or client ID.
  8. Check required application claims such as tenant, role, scope, or session ID.
  9. Reject tokens that are missing required claims, not merely expired tokens.

Many libraries verify the signature and time claims by default but make issuer and audience checks opt-in. Read your library's options instead of assuming.

JWT Payloads Are Not Private

Base64url is encoding. It is not encryption. A signed JWT can be tamper-evident and still be readable by anyone who has it.

Do not put these in a normal JWT payload:

  • Passwords
  • API keys
  • Refresh tokens
  • Full credit-card numbers
  • Medical or financial details
  • Private profile fields that should not appear in logs

If claims need confidentiality, use JWE or keep the sensitive data server-side and send a short session identifier. In many web apps, the simpler session ID design is easier to protect than encrypted self-contained tokens.

For the broader encoding mistake, see Base64 Is Not Encryption.

How to Decode a JWT Locally

Paste a token into JWT Decoder when you need to inspect a token quickly. The tool runs in your browser and returns a JSON object with header and payload. It accepts a copied Bearer ... value and removes the prefix before decoding.

Because real JWTs are bearer credentials, use a local-first workflow:

  • Prefer expired or test tokens when writing bug reports.
  • Do not paste production tokens into random third-party tools.
  • Redact tokens in screenshots and logs.
  • If you only need to inspect one section, decode the Base64url section rather than sharing the whole token.

Local decoding reduces exposure, but it does not make the token harmless. Anyone who receives an unexpired bearer token may be able to use it.

Troubleshooting JWT Decode Errors

Error or symptom Likely cause Fix
Not a JWT Missing dots, empty header, or empty payload Paste the compact token, not a JSON wrapper or quoted string
Invalid JWT header First section is not Base64url JSON Check for copy/paste truncation or a wrong token type
Invalid JWT payload Second section is not Base64url JSON Check for truncation, URL encoding, or encrypted JWE
Five dot-separated parts Compact JWE, not normal signed JWT Use a JWE library and decryption key
Expiry looks like 1970 Treated seconds as milliseconds Use new Date(exp * 1000)
Payload is readable but app rejects it Signature, issuer, audience, or expiry failed Verify with the same settings as the server

The last row is common: readability does not mean validity. A token can decode perfectly and still be expired, forged, intended for another audience, or signed by the wrong issuer.

Frequently Asked Questions

How do I decode a JWT?

Remove an optional Bearer prefix, split the token on dots, Base64url-decode the header and payload, decode the bytes as UTF-8, then parse each part as JSON. The signature section is not human-readable JSON.

Do I need a secret key to decode a JWT?

No. A key is not needed to decode the header and payload because they are only Base64url-encoded. A secret or public key is needed to verify the signature and prove the token is authentic.

Is decoding a JWT the same as verifying it?

No. Decoding shows what the token claims. Verification checks the cryptographic signature and required claims such as issuer, audience, expiration, and not-before. Never authorize a request from decoded claims alone.

Can anyone read the data inside a JWT?

Yes, for normal signed JWTs. The payload is Base64url-encoded and signed, not encrypted. Anyone with the token can read the claims, so do not put secrets or private personal data in the payload.

What does the exp claim mean?

exp is the expiration time as a Unix timestamp in seconds. In JavaScript, convert it with new Date(exp * 1000). Verification libraries should reject expired tokens before your app trusts any claim.

What if my token has five sections instead of three?

A five-part compact token is usually JWE, which means the payload is encrypted. A normal JWT decoder can split it, but it cannot read the claims without the decryption key and a JWE-capable library.

What is the kid header used for?

kid is a key ID in the JWT header. It helps the verifier choose the right key from a trusted JWKS or key set. Treat it as untrusted input and never use it directly as a file path, SQL value, or URL.

Inspect Tokens Locally

Sources

Last reviewed July 2026.