Skip to content
koducathome

How to Decode a JWT (and Why Decoding Isn't Verifying)

Decode a JSON Web Token's header and payload by hand or in code, read exp, nbf and iat claims, and see why only signature verification proves a token valid.

By the koducat teamUpdated 6 min read

What is inside a JWT

A JSON Web Token (JWT, defined in RFC 7519) is a compact string made of three parts separated by dots: a header, a payload and a signature. The header and payload are JSON objects encoded with base64url; the signature is binary data, also base64url-encoded.

Split at the dots for readability — a real token is a single line
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoding the first two parts of this well-known example token gives:

Header and payload
{ "alg": "HS256", "typ": "JWT" }

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

Base64url is ordinary base64 with two substitutions — - instead of + and _ instead of / — and with the trailing = padding removed, so the token is safe in URLs and HTTP headers. It is an encoding, not encryption: anyone who has the token can read the header and payload. Never put secrets such as passwords in a JWT payload.

How to decode a JWT online

  1. Copy the token, for example from an Authorization: Bearer … header in your browser's network panel. Copy only the token itself, not the word Bearer.
  2. Paste it into the JWT Decoder. The header and payload are decoded and pretty-printed immediately.
  3. Read the alg value in the header to see how the token was signed, and kid if present to see which key was used.
  4. Check the time claims. The decoder converts exp, nbf and iat to readable dates and tells you whether the token has expired.
  5. If you need to inspect a timestamp elsewhere, paste it into the Timestamp Converter — JWT times are in seconds, not milliseconds.
A token is a credential. Prefer decoders that work locally in your browser, and avoid pasting live production tokens into websites you do not trust. Expired or test tokens are the safest thing to share in bug reports.

Decoding a JWT in JavaScript, Python and bash

Decoding needs no library — only base64url decoding and JSON.parse. In Node.js, Buffer understands base64url directly:

Node.js
const [headerPart, payloadPart] = token.split(".");
const header = JSON.parse(Buffer.from(headerPart, "base64url").toString("utf8"));
const payload = JSON.parse(Buffer.from(payloadPart, "base64url").toString("utf8"));

In the browser, convert base64url back to base64, restore the padding and decode the bytes as UTF-8 (using atob alone breaks on non-ASCII characters such as accented names):

Browser
function decodeJwtPart(part: string): unknown {
  const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
  const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const [header, payload] = token.split(".").slice(0, 2).map(decodeJwtPart);
Python
import base64, json

def decode_part(part: str) -> dict:
    padded = part + "=" * (-len(part) % 4)
    return json.loads(base64.urlsafe_b64decode(padded))

header_b64, payload_b64, _signature = token.split(".")
print(decode_part(header_b64), decode_part(payload_b64))
bash with jq
# Prints the payload (field 2). Use 1 for the header.
jwt_part() {
  local s pad
  s=$(printf '%s' "$1" | cut -d. -f"$2" | tr '_-' '/+')
  pad=$(( (4 - ${#s} % 4) % 4 ))
  s="$s$(printf '%*s' "$pad" '' | tr ' ' '=')"
  printf '%s' "$s" | base64 -d
}

jwt_part "$TOKEN" 2 | jq .

Reading the standard claims

RFC 7519 defines a set of registered claim names. None are mandatory, but most identity providers use them, and verification libraries check several of them for you.

ClaimNameMeaning
issIssuerWho created and signed the token, often a URL
subSubjectWho the token is about, usually a user ID
audAudienceWhich API or client the token is intended for; a string or an array
expExpiration timeAfter this moment the token must be rejected
nbfNot beforeBefore this moment the token must be rejected
iatIssued atWhen the token was created
jtiJWT IDA unique identifier, useful for detecting replays or revoking tokens

exp, nbf and iat are NumericDate values: the number of seconds since 1970-01-01T00:00:00Z, not milliseconds. JavaScript's Date.now() returns milliseconds, so divide by 1000 before comparing:

const nowSeconds = Math.floor(Date.now() / 1000);
const expired = typeof payload.exp === "number" && payload.exp <= nowSeconds;
const notYetValid = typeof payload.nbf === "number" && payload.nbf > nowSeconds;

Servers usually allow a small clock-skew tolerance of a few seconds to a minute, because the issuer's clock and yours are never perfectly in sync.

Why decoding is not verifying

Decoding tells you what a token claims. It says nothing about whether those claims are true. Anyone can create a token with "role": "admin" in the payload — the JWT Generator does exactly that for testing. What makes a token trustworthy is the signature: a value computed over the header and payload with a secret (HMAC algorithms such as HS256) or a private key (RSA or ECDSA algorithms such as RS256 and ES256).

Verification recomputes or checks that signature with the expected key and then validates the claims. Use a maintained library rather than writing this yourself. With jose in JavaScript or TypeScript:

Verifying with jose against a JWKS endpoint
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));

const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
  issuer: "https://auth.example.com/",
  audience: "my-api",
  algorithms: ["RS256"], // never accept whatever alg the token asks for
});
Verifying with PyJWT (HS256 shared secret)
import jwt  # pip install PyJWT

payload = jwt.decode(
    token,
    key=SECRET,
    algorithms=["HS256"],
    audience="my-api",
    issuer="https://auth.example.com/",
)
Only make authorization decisions on claims from a token your server has verified. Decoding in the browser is fine for display — showing a user's name or when their session expires — but never for deciding what they are allowed to do.

Common JWT mistakes and attacks

  • The `alg: none` attack. RFC 7519 allows unsecured tokens with "alg": "none" and an empty signature. A library that trusts the header's alg may accept a forged token with no signature. Always pass an explicit allow-list of algorithms when verifying.
  • Algorithm confusion. If a server expects RS256 but lets the token choose its algorithm, an attacker can sign a token with HS256 using the server's public key as the HMAC secret. An explicit algorithm list prevents this too.
  • Not checking `aud` and `iss`. A valid token issued for a different application should not be accepted by yours.
  • Weak HMAC secrets. HS256 tokens can be brute-forced offline if the secret is short or guessable. Use a long random secret, at least 256 bits.
  • Long lifetimes. JWTs cannot easily be revoked before exp, so keep access tokens short-lived and use refresh tokens for longer sessions.
  • Sensitive data in the payload. The payload is readable by anyone who holds the token. If you need confidentiality, use encrypted JWE tokens or keep the data server-side.

For more security utilities — hashing, password generation and token tools — browse the security tools category.

Try it now with the free JWT Decoder — it runs in your browser.

Open JWT Decoder

Frequently asked questions

Can I decode a JWT without the secret key?

Yes. The header and payload are only base64url-encoded, so anyone can decode them. The secret or public key is needed to verify the signature, not to read the contents.

Why does my JWT show a date in 1970 or far in the future?

JWT time claims are in seconds. If code treats exp as milliseconds you get dates in January 1970; if it writes milliseconds into exp you get dates thousands of years ahead. Convert with seconds × 1000 when creating a JavaScript Date.

Is it safe to store JWTs in localStorage?

It is readable by any JavaScript on the page, so a cross-site scripting bug can steal the token. Many teams prefer HttpOnly, Secure cookies with SameSite protection for session tokens. Whichever you choose, keep tokens short-lived.

What is the difference between JWT, JWS and JWE?

JWT defines the claims format. A signed JWT is a JWS (JSON Web Signature) — the common three-part token. A JWE (JSON Web Encryption) token is encrypted, has five parts, and cannot be read without the decryption key.