How do I decode a JWT safely?

Dark navy cover card with the title 'How do I decode a JWT safely?' in white and a green accent bar

To decode a JWT safely, split the token on its dots and base64url-decode the header and payload on your own machine, or in a tool that runs entirely in your browser. Then treat what you read as unverified. Decoding shows you the claims, but only a signature check with the issuer's key tells you whether they are real.

What decoding a JWT actually does

Decoding a JWT turns its first two parts back into readable JSON, and nothing more. RFC 7519 defines a JWT as a sequence of URL-safe parts separated by period characters, where each part contains a base64url-encoded value.

A signed token looks like header.payload.signature:

  • Header: JSON naming the algorithm, such as {"alg":"HS256","typ":"JWT"}.
  • Payload: JSON with the claims, such as sub, exp and your own fields.
  • Signature: bytes computed over the first two parts with a key.

No key is involved in reading the header or payload. That is why anyone who holds a token can read it, and why a JWT payload is the wrong place for a password or an API key.

Why decoding is not the same as verifying

Decoding a JWT tells you what the token claims, while verifying tells you whether a trusted issuer made it. A forged token decodes exactly as cleanly as a real one.

The jwt-decode library's README says the library doesn't validate the token and that any well-formed JWT can be decoded. Validation belongs in your server-side logic.

RFC 8725, the JWT best practices document, describes the classic failure. An attacker can change the algorithm to none, and some libraries would trust that value and "validate" the JWT without checking any signature. So reading "alg":"none" in a decoded header is a warning sign, not a detail.

Three ways to decode a JWT safely

The safest place to decode a JWT is a place the token never leaves.

1. In your browser console or Node

You can decode a JWT safely with a few lines of JavaScript, but plain atob gets it wrong in two ways. RFC 4648 section 5 defines base64url as Base64 with - and _ in place of + and /, and padding may be left off. MDN's atob page says atob throws InvalidCharacterError on input that isn't valid Base64, and returns a binary string of raw bytes.

I tested both traps on a token whose payload was {"sub":"42","name":"José Müller","note":"a>b?","exp":1767225600}:

atob(payload)             -> InvalidCharacterError: Invalid character
atob(after swapping -/_)  -> {"sub":"42","name":"José Müller", ...}

The a>b? text produced a - in the encoded payload, and the accented name came back garbled because atob returns bytes, not UTF-8 text. This version handles both:

function decodeJwtPart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

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

In Node, Buffer.from(part, "base64url").toString("utf8") does the same in one call. The Base64 command line guide covers doing it from a terminal instead.

2. With a decode library in your app

In front-end code, a library such as jwt-decode saves you the byte handling. jwtDecode(token) returns the payload, and jwtDecode(token, { header: true }) returns the header. A malformed token throws InvalidTokenError, which is easier to handle than a raw parse error.

Use this for display only: showing a user's name, or reading exp to refresh a session early. Never let the decoded payload decide what a user may access.

3. In a client-side web tool

A web decoder is safe only if the token never leaves your page. Before you paste anything real, open DevTools, go to the Network tab, paste a sample token, and watch for requests that carry it. If you see one, close the tab.

Our JWT decoder decodes in the page: its tool code makes no network requests, and pasted input is not saved. It shows the header, payload and signature, plus the iat and exp times with an expired or valid flag. It does not verify the signature, so it can't tell you a token is genuine. The page also loads Google Analytics, fonts and icons from third-party servers; the token itself isn't sent anywhere. The online formatter safety guide explains how to check claims like these yourself.

Which tokens are safe to paste where

How careful you need to be depends on what the token can still do. A live token works like a password until it expires.

Token Where to decode it
Sample or test token Anywhere, including web tools
Expired production token Local tools or a client-side web tool
Live production token Your own console, terminal or app only

My take: I decode live tokens only in my own console, even with tools I trust. A browser extension, a shared screen or a pasted chat log can leak a token, and the console avoids all three. Reading exp first tells you which row of the table you are in.

How to read the claims you decoded

The standard claims in a decoded JWT tell you who issued it, who it is for, and when it is valid. RFC 7519 defines them:

  • iss: the issuer of the token.
  • aud: the intended recipients. If your service isn't in aud, the JWT MUST be rejected.
  • exp: the time on or after which the token MUST NOT be accepted.
  • nbf: the time before which it MUST NOT be accepted.
  • iat: when it was issued, which tells you its age.

Times are NumericDate values, seconds since 1970-01-01T00:00:00Z UTC. So 1767225600 is a count of seconds, while JavaScript's new Date() expects milliseconds. The JSON date format guide covers that mix-up. RFC 7519 also lets servers allow a small leeway, usually no more than a few minutes, for clock skew, so a token that looks just expired may still pass.

How to verify a JWT before you trust it

Verify a JWT on the server with a maintained library, the issuer's key and a fixed list of algorithms. RFC 8725 says libraries MUST let the caller specify the supported algorithms and MUST NOT use any others, which is your defence against alg: none.

  1. Pin the algorithms your issuer uses, for example only RS256.
  2. Verify the signature with the issuer's key, not one named inside the token.
  3. Check exp and nbf against the current time.
  4. Check that iss is the issuer you expect and that aud includes your service. RFC 8725 says the application MUST reject the JWT when the audience is missing or isn't associated with the recipient.

FAQ

Is it safe to decode a JWT in the browser?

Decoding itself is harmless, because the header and payload are only base64url-encoded. The risk is where the token goes: decode it on your own machine or in a tool that runs fully in the page, and don't paste live production tokens into sites that send them to a server.

Does decoding a JWT verify it?

No. Decoding only reads the header and payload. Any well-formed token decodes, including a forged one, so trust the claims only after a library verifies the signature with the issuer's key.

Why does atob fail on my JWT?

JWT parts use base64url, which uses - and _ where standard Base64 uses + and /. atob only accepts the standard alphabet, so swap those characters first, or use a base64url-aware decoder.

Can I read a JWT without the secret key?

Yes. The secret or public key is only needed to verify the signature. Anyone holding the token can read its claims, so don't put secrets in a JWT payload.

Sources