How to Decode a JWT and Read Its Claims Safely
Decode a JWT by splitting its three base64url segments, read claims like iss, sub and exp, and learn why decoding a token is not the same as verifying it.
Updated
A JWT is three base64url-encoded segments separated by dots: header, payload and signature. To read the claims, split on ., base64url-decode the first two segments, and parse the resulting JSON. Decoding needs no key, but it also proves nothing; only verifying the signature tells you the token is genuine.
Anatomy of a token
A typical JWT looks like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c. The three parts are:
- Header:
{"alg":"HS256","typ":"JWT"}. Says which algorithm signed the token and optionally a key id (kid). - Payload:
{"sub":"1234","name":"Ada","iat":1516239022}. The claims, which is the data you actually care about. - Signature: bytes produced by signing
header.payloadwith the issuer's key. Not JSON, so it does not decode to anything readable.
Every JWT header starts with eyJ because that is what {" encodes to in Base64. If you see eyJ, you are almost certainly looking at a JWT.
Decoding by hand
The segments use base64url, not standard Base64: - instead of +, _ instead of /, and no = padding. Most decoders accept the URL-safe alphabet but some insist on padding. To decode in a strict tool, replace the characters and append = until the length is a multiple of four. Then decode and you have JSON text.
In JavaScript the quick version is JSON.parse(atob(token.split(".")[1].replace(/-/g,"+").replace(/_/g,"/"))). In Python, base64.urlsafe_b64decode(seg + "=" * (-len(seg) % 4)) handles the padding arithmetic for you. On the command line, cut -d. -f2 | base64 -d works once padding is fixed.
If you just want to see what is inside without writing code, paste the token into the JWT Decoder. It splits the segments, decodes header and payload, explains each standard claim, and shows a countdown to expiry.
To inspect the bytes of a single segment yourself, drop it into the Base64 decoder after fixing the padding; the output is the raw JSON text.
The claims you will meet
Registered claims are short names defined in RFC 7519. The common ones:
iss(issuer): who created the token, usually a URL such ashttps://accounts.example.com.sub(subject): the user or principal the token is about.aud(audience): who the token is intended for. Your API should reject tokens whoseauddoes not match it.exp(expiration): Unix timestamp in seconds after which the token is invalid.nbf(not before): the token is invalid before this time.iat(issued at): when it was created.jti: unique token id, used to prevent replay.
Everything else is custom: email, roles, scope, tenant_id and so on. Custom claims mean whatever the issuer says they mean, so check your identity provider's documentation rather than guessing.
Timestamps are seconds, not milliseconds. 1516239022 is January 18, 2018. A value like 1516239022000 is a bug on the issuer's side and will be read as a date tens of thousands of years away by any correct validator.
Decoding is not verifying
This is the part people get wrong. Anyone can build a token with {"sub":"admin","roles":["superuser"]} in the payload and any string in the signature slot. It decodes perfectly. It is also worthless unless the signature checks out against the issuer's key.
Verification depends on alg:
HS256,HS384,HS512: HMAC with a shared secret. The verifier needs the same secret the issuer used. The signature is an HMAC-SHA digest ofheader.payload; a hash generator shows what a plain SHA-256 digest looks like, but HMAC also mixes in the key, so verification belongs in a library on your server.RS256,ES256,PS256: asymmetric. The issuer signs with a private key; you verify with the public key, typically fetched from a JWKS endpoint such as/.well-known/jwks.jsonand matched bykid.none: no signature. Reject these outright.
Use a maintained library (jsonwebtoken or jose in Node, PyJWT in Python, java-jwt on the JVM) and pass it the expected algorithm list, issuer and audience. Never let the token's own alg header decide how it is verified; that is the root of several real-world bypasses.
Decode to debug. Verify to trust. A decoded claim is a hint about what the issuer intended, not proof of anything, until the signature has been checked server-side.
Safe handling while debugging
A JWT is a bearer credential: whoever holds it can use it until it expires. Paste tokens only into tools that run in the browser and do not transmit them. Prefer expired tokens or ones from a staging environment when sharing in tickets. Strip the signature segment before posting a token in chat; the first two segments still show the claims and the token is no longer usable.
More decoding and inspection tools are in the developer category.
Try it
Paste a token into the JWT Decoder to see the header, every claim with a short explanation, the signing algorithm and how long until exp. Nothing leaves your browser.