JWT Decoder: Decode JSON Web Tokens (JWS) Online

Decode JSON Web Tokens in your browser. View header, payload, and claims without verification. Nothing uploaded.

JWT decoding splits a compact JWS token (three base64url-encoded segments separated by dots) and displays the header and payload as JSON. This tool does not verify signatures or check expiration — it only reveals the token contents. Nothing is uploaded.

Decode a JSON Web Signature

A JSON Web Token (JWT) in its most common form is a JSON Web Signature (JWS). The compact serialization consists of three base64url-encoded parts separated by dots:

header.payload.signature
  • Header: JSON object specifying the algorithm (alg) and token type (typ)
  • Payload: JSON object containing claims (sub, iat, exp, etc.)
  • Signature: Cryptographic signature over header and payload

Each part is encoded using base64url (RFC 4648 §5), which replaces + with - and / with _ to make tokens URL-safe.

Decoding is not verifying

Anyone can decode a JWT and read its payload — the base64url encoding is not encryption. The signature exists to prove the token was issued by a trusted party and has not been tampered with. To verify authenticity, you must check the signature using the issuer's public key (for RS256, ES256) or shared secret (for HS256). This tool only decodes; it does not verify signatures. For the security implications, see Compact JWS vs JWE: Decode Is Not Verify.

Compact JWT
Header
Payload
Signature (base64url, not verified)

Signature segment will appear here...

How it works

  1. Paste your JWT. Enter the compact token string. Bearer prefix is automatically removed.
  2. Click Decode. The tool splits the token on dots, base64url-decodes the header and payload, and parses them as JSON.
  3. View the contents. Header shows the algorithm and type. Payload shows the claims. NumericDate values (iat, exp) are converted to readable UTC timestamps.

Compact JWS vs JWE

This page counts compact segments first, then base64url-decodes only a three-part JWS (RFC 7515 / RFC 7519). Compact JWE (RFC 7516) is five segments and is rejected — the ciphertext is not decrypted. Decode is not HMAC, RSA, ECDSA, or JWKS verification, and it does not check exp or nbf. For the longer comparison, see Compact JWS vs JWE: Decode Is Not Verify.

AspectCompact JWSCompact JWE
SpecRFC 7515 Compact Serialization; JWT claims in RFC 7519RFC 7516 Compact Serialization
SegmentsThree: header.payload.signatureFive: header.encrypted_key.iv.ciphertext.tag
PayloadReadable JSON after RFC 4648 §5 decode — not encryptedEncrypted; this page never opens it
This pageDecodes header and payload as JSON objects; shows the signature segmentRejects with “Five segments is compact JWE (RFC 7516). This page does not decrypt.”
Unsecured formTrailing-dot empty signature is accepted (alg none shape); still not verifiedNot applicable
Other countsOne, two, or four segments fail: “Need three compact JWS segments”Only exactly five is treated as JWE

The sample compact JWS decodes to header {"alg":"HS256","typ":"JWT"} and payload sub user-42, name Ada Lovelace, iat 1516239022 → 2018-01-18T01:30:22.000Z, exp 1516242622 → 2018-01-18T02:30:22.000Z. Those NumericDate seconds are display-only — the same POSIX-seconds clock as Unix timestamp Seconds mode, not an expiration check. The third segment bm90LWEtcmVhbC1zaWduYXR1cmU is ASCII not-a-real-signature, not an HMAC over the first two parts. A leading Bearer prefix and interior whitespace are stripped before the count. JOSE JSON serialization (JWS or JWE objects, not compact dots) is out of scope. Encoding UTF-8 as RFC 4648 §5 belongs on Base64url, not this decoder.

Decode a JWT in Python and Node

The snippets below split a compact JWS (RFC 7515 / RFC 7519), base64url-decode the first two segments (RFC 4648 §5), and parse them as JSON — the same decode-only path this page uses. They do not verify HMAC, RSA, or ECDSA signatures, and they do not check exp or nbf. For why decode is not verify, see Compact JWS vs JWE: Decode Is Not Verify.

Node.js with Buffer

Node 16+ Buffer encoding 'base64url' accepts unpadded JWT segments. Split on dots, then UTF-8-decode each JSON segment. Five segments is compact JWE — this page does not decrypt those:

const token =
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.bm90LWEtcmVhbC1zaWduYXR1cmU';

const [headerSeg, payloadSeg] = token.split('.');
const header = JSON.parse(Buffer.from(headerSeg, 'base64url').toString('utf8'));
// { alg: 'HS256', typ: 'JWT' }

const payload = JSON.parse(Buffer.from(payloadSeg, 'base64url').toString('utf8'));
// { sub: 'user-42', name: 'Ada Lovelace', iat: 1516239022, exp: 1516242622 }

Python with base64.urlsafe_b64decode

CPython urlsafe_b64decode maps - / _ but still needs = padding to a multiple of four. Restore padding before json.loads. PyJWT's jwt.decode(..., options={"verify_signature": False}) is the same unverified read — it is not HMAC verification:

import base64
import json

def b64url_decode(segment):
    pad = '=' * ((4 - len(segment) % 4) % 4)
    return base64.urlsafe_b64decode(segment + pad)

token = (
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
    'eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.'
    'bm90LWEtcmVhbC1zaWduYXR1cmU'
)
header_b64, payload_b64, _sig = token.split('.')
header = json.loads(b64url_decode(header_b64))
# {'alg': 'HS256', 'typ': 'JWT'}

payload = json.loads(b64url_decode(payload_b64))
# {'sub': 'user-42', 'name': 'Ada Lovelace', 'iat': 1516239022, 'exp': 1516242622}

The third segment is the signature bytes, not JSON. That token's signature decodes to the ASCII string not-a-real-signature — reading it still does not prove the header or payload were signed. Use a JWT library with the issuer's key when you need verification.

Decode a JWT in the shell

POSIX cut -d. splits a compact JWS. GNU and BSD base64 decode RFC 4648 §4 (+ / /), not JWT's RFC 4648 §5 (- / _). JWT segments also omit = padding. This page's compact JWS happens to use only A–Z a–z 0–9 in all three segments, so the alphabet map is a no-op. The header is 36 characters (a multiple of 4); GNU coreutils 9.4 base64 -d writes {"alg":"HS256","typ":"JWT"}. The payload is 98 characters. The same command writes the claims JSON, then base64: invalid input (exit 1). Restore == first. The signature is 27 characters — it writes ASCII not-a-real-signature, then the same error; restore =. These commands do not verify HMAC, RSA, or ECDSA.

GNU base64 (Linux)

cut -d. -f1 is the header; -f2 is the payload. GNU base64 -d is §4. Map - / _ with tr -- '-_' '+/' and pad to a multiple of 4 before decode. A compact JWS whose payload contains _ (eyJ4IjoiPj4_In0) needs that map; this page's payload does not:

TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.bm90LWEtcmVhbC1zaWduYXR1cmU'

printf '%s\n' "$TOKEN" | cut -d. -f1 | base64 -d
# {"alg":"HS256","typ":"JWT"}

printf '%s\n' "$TOKEN" | cut -d. -f2 | base64 -d
# {"sub":"user-42","name":"Ada Lovelace","iat":1516239022,"exp":1516242622}
# base64: invalid input

printf '%s==\n' \
  'eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ' \
  | base64 -d
# {"sub":"user-42","name":"Ada Lovelace","iat":1516239022,"exp":1516242622}

printf '%s' 'eyJ4IjoiPj4_In0' | tr -- '-_' '+/' \
  | awk '{n=(4-length($0)%4)%4; printf "%s",$0; for(i=0;i<n;i++) printf "=";}' \
  | base64 -d
# {"x":">>?"}

GNU basenc --base64url

Coreutils 8.31+ basenc --base64url -d is RFC 4648 §5 — the same alphabet this page uses. It still requires = padding when the length is not a multiple of 4. Unpadded, this payload prints the claims JSON, then basenc: invalid input. macOS has no basenc; Homebrew coreutils installs gbasenc:

printf '%s\n' "$TOKEN" | cut -d. -f1 | basenc --base64url -d
# {"alg":"HS256","typ":"JWT"}

printf '%s==\n' \
  'eyJzdWIiOiJ1c2VyLTQyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ' \
  | basenc --base64url -d
# {"sub":"user-42","name":"Ada Lovelace","iat":1516239022,"exp":1516242622}

printf '%s' 'Pj4-Pz8_' | basenc --base64url -d
# >>>???

jq @base64d

jq 1.7 @base64d is §4 and more lenient about padding than GNU base64. Pass -R so the compact JWS is a string — without it, jq 1.7 errors Invalid numeric literal. -r writes the decoded JSON without extra quotes. @base64d on eyJ4IjoiPj4_In0 fails (is not valid base64 data); map - / _ first. jq is not POSIX and is not HMAC verification:

printf '%s\n' "$TOKEN" | jq -Rr 'split(".") | .[0],.[1] | @base64d'
# {"alg":"HS256","typ":"JWT"}
# {"sub":"user-42","name":"Ada Lovelace","iat":1516239022,"exp":1516242622}

printf '%s\n' 'eyJ4IjoiPj4_In0' | jq -Rr '@base64d'
# jq: error (at <stdin>:1): string ("eyJ4IjoiPj...) is not valid base64 data

printf '%s\n' 'eyJ4IjoiPj4_In0' \
  | jq -Rr 'gsub("-";"+") | gsub("_";"/") | @base64d'
# {"x":">>?"}

macOS ships BSD base64 — decode with -D (or -d on recent releases). OpenSSL 3.0 base64 -d -A on the unpadded payload writes the claims JSON without the closing } and exits 0 — pad first. A five-segment compact JWE still has a first and second field; cut -d. -f1,2 prints them. This page rejects five segments and does not decrypt. For why decode is not verify, see Compact JWS vs JWE: Decode Is Not Verify. Encoding UTF-8 as RFC 4648 §5 belongs on Base64url, not this decoder.

FAQ

Does this decoder verify the JWT signature?
No. This tool only decodes and displays the header and payload. It does not validate HMAC, RSA, ECDSA signatures, or check expiration. Use a proper JWT library in your application for verification.
Does this tool upload my token?
No. All decoding runs in your browser using base64url decoding. Your JWT is never sent to a server.
What JWT formats are supported?
Compact JWS (RFC 7515 / RFC 7519): three base64url segments separated by dots. An optional Bearer prefix is automatically stripped. JWE (five segments) is not decrypted.
What are iat and exp claims?
Both are NumericDate values — Unix timestamps in seconds since 1970-01-01T00:00:00Z. This tool displays them as ISO 8601 UTC strings for readability, but does not validate expiration.
Why does decoding fail?
Common causes: missing or extra segments, invalid base64url characters, or header/payload that is not valid JSON. Ensure the token has exactly three dot-separated segments.
How do I decode a JWT in JavaScript?
Split the token on dots, then base64url-decode the first two segments. Replace - with + and _ with /, add padding, and use atob(): atob(segment.replace(/-/g, "+").replace(/_/g, "/") + "==".slice((2 - segment.length * 3) & 3)). Then JSON.parse the result. For production use, the jwt-decode library handles edge cases. Decoding does not verify the signature — use jose or jsonwebtoken to verify tokens with the issuer's key.

Related