What's Inside a JWT? Decode It, Verify It, and Avoid the alg:none Trap

Reckonist EditorialPublished July 20, 20265 min read

You paste a token mid-incident to read one claim — is it expired? which alg? whose sub? — and a wall of eyJ... stares back. A JWT looks encrypted. It isn't. It's three base64url-encoded chunks anyone can read, and understanding that distinction is the difference between reading a token and trusting one.

The three parts

A JSON Web Token (JWT, defined by RFC 7519) is three base64url strings joined by dots. The compact serialization comes from JWS, RFC 7515:

base64url(header) "." base64url(payload) "." base64url(signature)

Split on the two dots and you have:

  1. Header — JSON metadata describing the token. Which signature algorithm (alg) and token type (typ).
  2. Payload — the JSON claims: who the token is about, when it expires, what it's allowed to do.
  3. Signature — a MAC or digital signature over header.payload, the only part that proves the token wasn't forged or tampered with.

Only the third part involves a key. The first two are plain, reversible encoding.

base64url, not base64

JWT uses base64url (RFC 4648 §5), a URL-safe variant: + becomes -, / becomes _, and the trailing = padding is dropped. That's why a JWT survives being placed in a URL or an Authorization header without escaping. If you feed a JWT segment to a standard base64 decoder that expects padding, it may choke — handle the missing = and the -/_ alphabet.

Reading the payload: the standard claims

The payload carries registered claims (from the IANA JWT registry) plus whatever custom claims your app adds. The ones you'll see constantly:

ClaimNameMeaning
issIssuerWho minted the token
subSubjectWho/what the token is about (a user id)
audAudienceWho the token is intended for
expExpirationToken invalid at or after this time
nbfNot BeforeToken invalid before this time
iatIssued AtWhen the token was created
jtiJWT IDUnique identifier for the token

The one that trips people up: exp, nbf, and iat are NumericDate values — Unix epoch seconds, not milliseconds. Paste an exp into a converter that assumes milliseconds and you'll land in 1970.

Worked example

Take a payload that decodes to:

{ "sub": "1234567890", "name": "Jane", "iat": 1516239022, "exp": 1516242622 }

iat 1516239022 is 2018-01-18 01:30:22 UTC. exp 1516242622 is exactly one hour later, 02:30:22 UTC. So this token had a 60-minute lifetime. If the current time is past exp, the token is expired — regardless of whether its signature is valid. Expiry and authenticity are two separate checks.

HS256 vs RS256: symmetric vs asymmetric

The header's alg names the signing algorithm (RFC 7518 defines them). The two you'll meet most:

  • HS256 — HMAC with SHA-256. Symmetric: the same shared secret both signs and verifies. Simple, but everyone who can verify can also forge, so the secret must stay server-side.
  • RS256 — RSA signature with SHA-256. Asymmetric: a private key signs, and a public key verifies. The verifier only needs the public key (often published as a JWKS), which can't be used to mint tokens.

ES256 (ECDSA on the P-256 curve) is the elliptic-curve equivalent of RS256 — asymmetric, smaller keys. As a rule: HS256 when one service both issues and checks tokens; RS256/ES256 when an identity provider issues and many independent services verify.

Decoding is not verifying

Here's the part that matters most. Decoding a JWT only base64url-decodes it. The payload is plaintext to anyone who holds the token — there is no confidentiality. Never put a password, a card number, or any secret in a JWT payload expecting it to be hidden.

Trust comes only from verifying the signature with the correct key:

HS256:  HMAC-SHA256(secret, header"."payload) == signature   // needs the shared secret
RS256:  RSA-PKCS1-v1_5 verify with the public key             // needs the public key / JWKS
ES256:  ECDSA P-256 verify with the public key

A decoder that shows you the claims has proven nothing about who issued the token. That's fine for debugging — reading exp or scope during an incident — but a server accepting a token for auth must verify the signature, check exp/nbf, and confirm iss/aud match what it expects.

The alg:none trap

Because the header carries alg, an early and now-classic vulnerability is alg: none. The attacker takes a valid token, edits the payload (say, "role":"admin"), sets the header to {"alg":"none"}, drops the signature entirely, and submits it. A naive library that "verifies using whatever algorithm the header says" sees none, performs no verification, and accepts the forged token.

The rule: an unsigned token must never be treated as verified. Reject alg: none outright.

The HS/RS key-confusion attack

The subtler cousin exploits the same "trust the header's alg" mistake. A service verifies RS256 tokens with an RSA public key. An attacker takes a token, flips the header from RS256 to HS256, and signs it using the RSA public key as the HMAC secret. If the verifier reads alg: HS256 and dutifully runs HMAC with its configured RSA public key as the secret — the signature matches. And that public key is, by definition, public. Anyone can forge a "valid" token.

Both attacks share one root cause: letting the token decide how it gets verified. The fix is the same for both — pin the expected algorithm to the key type on the server side. If you expect RS256, verify only as RS256; never let the incoming header choose the algorithm, and never accept none.

Debug safely, in your browser

When you need to read a token, do it somewhere the token never leaves your machine — pasting a production JWT into a random web form is exactly the kind of leak these tokens are designed to prevent. Our JWT decoder decodes the header and payload, computes a live expired / valid / not-yet-valid badge from exp and nbf, and can verify HS256/RS256/ES256 signatures entirely client-side using the browser's Web Crypto API — no server round-trip.

Read the claims to debug. Verify the signature to trust. Never confuse the two.


Sources: RFC 7519 (JWT), RFC 7515 (JWS compact serialization), and RFC 7518 (JWA / signature algorithms).

Try it yourself

§ Calculators

The formulas above are built into Reckonist’s free calculators. Enter your numbers and see your result instantly.