JWT Attacks: The Complete Practical Guide

Anatomy of a JWT: Header, Payload, and Signature

How a JWT is built: the three base64url parts, the header params that attacks abuse (alg, kid, jwk, jku), the common claims, and how the signature is computed.

Published

Before you can attack a JWT you need to read one fluently. The structure is simple and the same every time, and once you can pick out the algorithm, the claims, and the dangerous header parameters at a glance, every attack in this cluster reads clearly. This page is the map. Practice decoding on a free live target.

Three parts, two dots

A JWT is three base64url-encoded strings joined by dots:

header.payload.signature

A real token looks like this (line-wrapped for readability, it is one string):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3aWVuZXIiLCJyb2xlIjoidXNlciJ9.VK9xT...signature...

Each part is independently base64url-decodable. The dots are separators, not part of the encoding. Decode the first part and you get the header, decode the second and you get the payload. The third is the signature, which is raw bytes rather than readable JSON.

base64url, not base64

The parts use base64url, a URL-safe variant of base64. Two differences matter when you decode by hand:

  • + becomes - and / becomes _.
  • The trailing = padding is stripped.

So to decode a part with a standard base64 tool, translate the alphabet back and re-add padding first:

echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | tr '_-' '/+' | base64 -d
# {"alg":"HS256","typ":"JWT"}

Skipping that step is the usual reason a quick decode "fails." Most JWT tooling handles base64url for you.

The header

The header is a small JSON object describing how the token is signed. The two standard fields:

{ "alg": "HS256", "typ": "JWT" }
  • alg is the signing algorithm, for example HS256 (HMAC) or RS256 (RSA). This field is the target of two different attacks: setting it to none, and algorithm confusion.
  • typ is the token type, usually JWT.

The header can also carry optional parameters that tell the server which key to use to verify the token. These are the ones later spokes abuse:

  • kid a key id, used to look up the verifying key by name.
  • jwk an embedded public key, inside the token itself.
  • jku a URL pointing at a JSON Web Key Set (JWKS) of keys.
  • x5u / x5c the certificate equivalents of jku and jwk.

If a server trusts any of these to choose the key, a token can nominate a key the attacker controls. That is covered in JWK, JKU, and KID injection.

The payload

The payload is a JSON object of claims, the statements the server trusts once the signature checks out:

{
  "sub": "wiener",
  "role": "user",
  "iat": 1700000000,
  "exp": 1700003600
}

Common registered claims:

  • sub the subject, typically the user id or name.
  • iat issued-at, a timestamp.
  • exp expiry, after which the token should be rejected.
  • nbf not-before, a time until which the token is not yet valid.
  • iss and aud the issuer and intended audience.

Plus whatever application claims the app adds, like role, admin, or tenant. Those application claims are what you rewrite once you can sign, and exp/nbf are what you tamper with to keep a token alive. Note the payload is readable, so these are never secret.

The signature

The signature is what makes the token tamper-evident. For HS256 it is an HMAC computed over the encoded header and payload joined by a dot, keyed with the server's secret:

signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

For RS256 it is an RSA signature over the same input, made with the server's private key and verified with its public key. Either way, the signature covers the header and payload, so if you change a single claim the signature no longer matches, and a server that verifies properly rejects the token. The entire JWT attack surface is about defeating that check: skipping it, recovering the key, or tricking the server into using a key you control.

Reading a token by eye

With the structure in hand, your first move against any token is always the same: split on the dots, base64url-decode the first two parts, and look. What algorithm is in the header? Is there a kid, jwk, or jku? What claims are in the payload, and which one decides your privileges? That thirty-second read tells you which attack to try first.

From here, go to flawed signature verification for the fastest bug, or weak secret brute force if the token is HS256. Or decode a live one now on a free target.

← Back to JWT Attacks: The Complete Practical Guide