~/learn / authorization / jwt
JSON Web Tokens in Depth
What a JWT actually contains, exactly how the signature is computed and verified, the algorithm-confusion and key-injection flaws step by step, the validation checklist every server must follow, and how revocation really works.
A JSON Web Token is a compact, signed bundle of claims. It shows up as OAuth access tokens, OIDC ID tokens, API keys, and session values. Paste any token into the JWT decoder as you read, and watch the three parts come apart.
Anatomy
A JWT is three Base64url-encoded parts joined by dots:
eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0 . eyJzdWIiOiIxMjMiLCJleHAiOjE3Njc... . kZ8pW3...
header payload signature
Header. Metadata, chiefly the signing algorithm alg and often a key id kid.
{ "alg": "RS256", "typ": "JWT", "kid": "k1" }
Payload. The claims. The registered ones have short names:
| Claim | Meaning |
|---|---|
iss |
Issuer, who created the token |
sub |
Subject, usually the user id |
aud |
Audience, who the token is for |
exp |
Expiry, Unix seconds |
nbf |
Not valid before |
iat |
Issued at |
jti |
Unique token id, useful for revocation |
Signature. A cryptographic signature over the first two parts.
The most important fact about a JWT: the payload is encoded, not encrypted. Base64url is reversible by anyone, so every claim is readable by whoever holds the token. Never put secrets or sensitive personal data in one. (Encrypted tokens exist, JWE, but they are rare.)
Exactly how the signature works
The signature is computed over the literal Base64url header and payload, joined by a dot. For HS256:
signature = HMAC-SHA256(key, base64url(header) + "." + base64url(payload))
To verify, the server recomputes that HMAC over the received header and payload and checks it equals the received signature (in constant time). Because any change to the header or payload changes the input, a tampered claim produces a signature mismatch, and the attacker cannot recompute the correct signature without the key. For RS256 the same input bytes are signed with an RSA private key and verified with the public key. This is the entire security model, and every classic flaw below is a way to make the server skip or subvert this check.
Symmetric versus asymmetric signing
| Family | Algorithms | Who can create | Who can verify |
|---|---|---|---|
| HMAC | HS256, HS384, HS512 | Anyone with the shared secret | Anyone with the shared secret |
| RSA and ECDSA | RS256, PS256, ES256 | Only the private key holder | Anyone with the public key |
| EdDSA | Ed25519 | Only the private key holder | Anyone with the public key |
With HMAC, every service that can verify a token can also forge one, because the key is the same both ways. With asymmetric algorithms, verifiers hold only the public key and cannot mint tokens. For anything beyond a single service, asymmetric signing is the safer default.
The classic flaws, mechanism by mechanism
Each has a free lab on PortSwigger Web Security Academy.
alg: noneaccepted. The spec defines an “unsecured” JWT withalg: noneand an empty signature. A library that honors it will treat an unsigned token as valid, so an attacker edits the payload, setsalgtonone, drops the signature, and becomes admin. Fix: never acceptnone; pin the algorithm.- Algorithm confusion (RS256 to HS256). The server expects RS256 and holds the RSA public key, which is, by definition, public. The attacker changes
algto HS256 and signs the token using the RSA public key bytes as the HMAC secret. A naive library verifies HS256 with “the configured key,” which is that same public value, and the forged token passes. The server’s own public key became the forgery key. Fix: select the verification algorithm from server configuration, not from the token header, so an HS256 token is never checked against an RSA key. - Weak HMAC secret. A short or dictionary HMAC secret can be cracked offline from a single captured token by trying candidate secrets until one reproduces the signature. Fix: long random secrets, or asymmetric keys.
- Trusting
jkuorx5u. These header fields point the server at a URL to fetch the verification key. If honored, the attacker hosts their own key and signs with the matching private key. Fix: never fetch keys from token-supplied URLs; use a fixed, configured JWKS endpoint. kidinjection. The key id selects which key to use. If the server interpolates it into a file path (keys/<kid>.pem) or a SQL query, the attacker injects path traversal or SQL to point at a key they control or a predictable value. Fix: validatekidagainst a known allowlist of ids.- No
expcheck. A token with no expiry check is valid forever, so one leak is permanent. Fix: always enforceexp.
The validation checklist
A server accepting a JWT must, in order:
- Pin the expected algorithm in configuration. Never let the token’s
algchoose. - Verify the signature with the correct key for that issuer.
- Check
expandnbf, allowing only small clock skew (a minute or two). - Check
issandaudagainst exact expected values. - Treat
kid,jku,x5uas untrusted input. Resolve keys only from a fixed, configured set (a cached JWKS).
Use a well-maintained library and configure it explicitly. Nearly every famous JWT CVE is one of these steps skipped.
Storage and revocation
- In the browser, prefer an
HttpOnly,Secure,SameSitecookie, which keeps the token away from JavaScript and therefore away from XSS.localStorageis readable by any script that runs on the page, so one XSS exfiltrates the token. - Revocation is the genuine hard part. A self-contained token stays valid until
expno matter what, because verification touches no database. The practical answer is short-lived access tokens (five to fifteen minutes) paired with longer refresh tokens the server can revoke, plus ajtidenylist for emergencies. Password change and logout should invalidate refresh tokens, and ideally bump a per-user token version that access tokens carry and the server checks.
Field notes: the attack checklist on a real token
Paste any JWT into jwt_tool or jwt.io and work the header first, because the signature algorithm is where tokens die:
alg: none. Set the header algorithm tonone(also tryNone,NONE,nOnE, since some libraries only blocklist the lowercase spelling), drop the signature, keep the trailing dot. If the server accepts it, you forge any claims you like.- RS256 to HS256 confusion. The server holds an RSA public key to verify RS256. If you switch the header to HS256, a naive library verifies the HMAC using that public key as the secret, and the public key is not secret. Sign your tampered token with the server’s public key as the HMAC key:
# You have the server's public key (from JWKS, a cert, or TLS). Forge an admin token:
python3 jwt_tool.py <token> -X k -pk public.pem # jwt_tool's key-confusion mode
- Weak HMAC secret. HS256 with a guessable secret cracks offline at enormous speed. Capture one token and run hashcat:
hashcat -m 16500 token.jwt /usr/share/wordlists/rockyou.txt
A hit means you can mint tokens for anyone. This is why HMAC secrets must be long and random, not secret or the app name.
Field notes: header injection via jku, x5u, and kid
Modern libraries let the token’s own header say where to find the verification key, which is a gift if the server trusts it:
jku/x5u: these point to a JSON Web Key Set or cert URL. If the server fetches the key from the URL in the token, host your own key at a URL you control and sign with its private half. Often chainable with SSRF to a same-origin or allowlisted host. The fix is a pinned, allowlisted key source, never the token’s.kidinjection: the key id selects which key to use, and it frequently flows into a file path or a SQL lookup."kid": "../../dev/null"makes the key an empty file so you can sign with an empty secret, and"kid": "x' UNION SELECT 'mysecret'-- "can return an attacker-known key through SQL injection. Any token-controlled key selection is a finding.
Run jwt_tool’s --playbook against a target to sweep all of these at once. Only against tokens from systems you are authorized to test.
Takeaway
A JWT is exactly as trustworthy as the server’s validation of it, and no more. Decode tokens freely to learn their structure, but remember the interesting question in any assessment is always what the server does with a token it should have rejected.