Web Security (OWASP Top 10:2025) Module 8 – A07:2025 Authentication Failures
JWT validation and token storage
Validate every JWT the way RFC 8725 asks, close the algorithm, key and claim bugs with tests, and choose where a browser app keeps its tokens.
What you will learn
- Validate a JWT by checking its algorithm, key, signature, type, issuer, audience and time window, in that order
- Recognise alg none, algorithm confusion, key ID and header-supplied key bugs in code review, and name the fixture that fails on each
- Design key rotation, token lifetimes and revocation with numbers for the window that each leaves open
- Choose where a browser app keeps its tokens, given what injected script can read and what a browser sends on its own
- Decide when a JWT suits a session and when an opaque session ID does
Before you start
What you’ll learn · Before you start
What you will learn
- Validate a JWT by checking its algorithm, key, signature, type, issuer, audience and time window, in that order
- Recognise alg none, algorithm confusion, key ID and header-supplied key bugs in code review, and name the fixture that fails on each
- Design key rotation, token lifetimes and revocation with numbers for the window that each leaves open
- Choose where a browser app keeps its tokens, given what injected script can read and what a browser sends on its own
- Decide when a JWT suits a session and when an opaque session ID does
Before you start
On this page
A service receives a token that claims "role": "admin". Whether to believe it is the whole question of JWT
validation. A JWT is three base64url strings that anyone can read and anyone can write, so in this lesson’s first
program a service that only decodes believes all four tokens it is shown, two of them forged, and a service that
verifies accepts one.
In short: verify the signature with a key you chose and an algorithm you allow, then check the issuer, the audience, the time window and the type. Where the token lives afterwards is a second question, token storage, because each place a browser app can keep a token has different readers. This lesson covers both, and the bugs that skip the checks.
A token can be read by anyone, so a service must check it before it believes it
A JWT: JSON Web Token (RFC 7519): three base64url parts joined by dots, a header, a payload of claims and a signature. The payload is encoded and not encrypted, so anyone who holds a token can read it. carries a header, a payload of claims and a signature. The payload is encoded and not encrypted, so anyone who holds a token can read its claims, and anyone can write a token with claims of their choice. JWT validation is what tells the two apart: it proves that the issuer signed these exact claims, and that this service is the one meant to read them. Step through the order in which a service makes its checks:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Split and read. A JWT is three base64url parts, a header, a payload and a signature. Splitting and decoding reads them and proves nothing, because anyone can write any header and any payload.
- Is the algorithm allowed?. The header names an algorithm. The name is a question for the service, not an instruction. If it is not on the service's own list, the check ends there, whatever the rest of the token looks like.
- Which key?. The header may name a key. The name is looked up in the service's own list of keys. A name that no key has ends the check, and a key that arrives inside the token is never used.
- Verify the signature. Only now is the signature computed with the chosen key and algorithm and compared with the token's. A signature that does not match ends the check, and nothing in the payload has been believed.
- Believe the claims. With a valid signature the claims are statements by the issuer. Each one is compared with what this service expects, namely the type of the token, the issuer, the audience and the time window.
- Accept or refuse. A token that passes every check is accepted and its claims are used. A token that fails any one of them is refused with a 401, and the reason goes to the log and not to the caller.
JWT validation has seven parts, and a service that skips any one of them has a bug that a test can show:
- Signature. The service recomputes it with the key it trusts and refuses a token whose signature does not match.
- Algorithm. The token’s
algmust be on the service’s own list, andnoneis never on it. - Key. The key comes from the service’s own list, found by the token’s
kid, and a key inside the token is never used. - Issuer. The
issclaim must name the issuer that the service trusts. - Audience. The
audclaim must name this service, so a token made for another API is refused. - Time. The token must be inside its
nbfandexpwindow, allowing a small leeway for clocks. - Type. The
typheader must say what kind of token it is, so an ID token cannot pass as an access token.
The first program makes four tokens, one genuine and three that are not, and prints what a decoder and a verifier say about each:
"""Decoding a JWT only reads it. Verifying it with the key is what makes its claims worth believing."""
import base64
import hashlib
import hmac
import json
KEY = b"lab key of the orders service: never use a key like this"
OTHER_KEY = b"a key that the sender made up"
NOW = 1_000_000 # seconds on the lab's own clock
def b64u(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def unb64u(text):
return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
def encode(obj):
return b64u(json.dumps(obj, separators=(",", ":"), sort_keys=True).encode())
def make(payload, key):
signing_input = f"{encode({'alg': 'HS256', 'typ': 'JWT'})}.{encode(payload)}"
return f"{signing_input}.{b64u(hmac.new(key, signing_input.encode(), hashlib.sha256).digest())}"
def decode_only(token):
"""Reads the payload. Anyone can do this: it is base64url text, not encrypted."""
return json.loads(unb64u(token.split(".")[1]))
def verify(token, key, now):
"""Checks the signature with the key first, and only then looks at the claims."""
head, body, signature = token.split(".")
expected = hmac.new(key, f"{head}.{body}".encode(), hashlib.sha256).digest()
if not hmac.compare_digest(expected, unb64u(signature)):
return None, "bad signature"
claims = json.loads(unb64u(body))
if now > claims["exp"]:
return None, "expired"
return claims, None
genuine = make({"sub": "asha", "role": "member", "exp": NOW + 900}, KEY)
head, _, signature = genuine.split(".")
edited = f"{head}.{encode({'sub': 'asha', 'role': 'admin', 'exp': NOW + 900})}.{signature}"
expired = make({"sub": "asha", "role": "member", "exp": NOW - 60}, KEY)
other_key = make({"sub": "asha", "role": "admin", "exp": NOW + 900}, OTHER_KEY)
print("what anyone can read in the genuine token:", decode_only(genuine))
print()
print(f"{'token':<30}{'decode only says':<20}verify says")
for name, token in [("genuine", genuine), ("payload edited, old signature", edited), ("expired", expired), ("signed with another key", other_key)]:
claims = decode_only(token)
said = f"{claims['sub']}, {claims['role']}"
checked, reason = verify(token, KEY, NOW)
answer = f"accepted: {checked['sub']}, {checked['role']}" if checked else f"refused: {reason}"
print(f"{name:<30}{said:<20}{answer}") Output
what anyone can read in the genuine token: {'exp': 1000900, 'role': 'member', 'sub': 'asha'}
token decode only says verify says
genuine asha, member accepted: asha, member
payload edited, old signature asha, admin refused: bad signature
expired asha, member refused: expired
signed with another key asha, admin refused: bad signature
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decode_vs_verify.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The decoder reported admin for the two forged tokens, because it believes what it reads. The verifier accepted only the
genuine one: the edited payload and the token signed with another key failed the signature, and the expired one failed
its exp.
Token storage is the other half. localStorage and sessionStorage are open to every script of the page. A variable in
memory is lost at every reload. An HttpOnly cookie is hidden from scripts, and the browser sends it with requests on
its own. A backend for frontend (BFF): A server component between a browser app and the APIs it calls. It runs the sign-in flow and keeps the app's tokens itself, so that the browser holds only a session cookie and no token. More in “OAuth 2.0 and OpenID Connect: done right” keeps the token on the server, so there is nothing in the browser
to read. Each place has its own readers.
Checkpoint
Check: Decoding reads, verifying believes · Where a browser keeps a token
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why must a service verify a JWT and not only decode it?
A JWT is three base64url strings; the payload is encoded, not encrypted, so anyone can read the claims, and anyone can write a token with claims of their choice. In the lesson a decoder believed all four tokens, two of them forged, while a verifier accepted only the genuine one.
-
Which seven parts of JWT validation can a test show are skipped?
Signature (recomputed with a key you trust); algorithm (the token's alg must be on your own list, never none); key (found by kid in your own list, never a key inside the token); issuer (iss); audience (aud names this service); time (nbf and exp, with a small leeway for clocks); and type (typ, so an ID token cannot pass as an access token).
-
Who can read a token kept in localStorage, in memory, in an HttpOnly cookie or in a backend for frontend?
localStorage and sessionStorage: every script of the page. A variable in memory: lost at every reload. An HttpOnly cookie: hidden from scripts, and the browser sends it with requests on its own. A backend for frontend keeps the token on the server, so there is nothing in the browser to read. Each place has its own readers.
-
Which kinds of bad token did the lesson's verifier refuse?
An edited payload and a token signed with another key failed the signature check, and an expired token failed its exp; only the genuine token passed.
Seven checks against fifteen tokens: a first draft and a fixed verifier Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Four bugs to know on sight in code review
- Keys rotate in three steps: publish, switch after the caches, remove last
- Lifetimes decide how long a revoked token keeps working, and shortening them has a price
- Where a browser keeps a token: five places, and what a script in the page can do
- When a JWT is the wrong token for a session
- Practise: a verifier that checks before it believes
- A JWT verifier that checks before it believes
- Interview questions
- What is the difference between decoding and verifying a JWT?
- A pull request verifies tokens with a library call that gets only the token and a key. Another route decodes a token without verifying it, to read the role. What do you ask for in each, and which tests do you require?
- After a signing-key rotation, about 3 % of requests to your API return 401 for roughly ten minutes and then recover. Explain what happened, and write the rotation plan that loses nothing.
- A single-page app keeps its access and refresh tokens in localStorage, and a review flags it. What do you change, and what does your change not fix?
- Key takeaways
1 diagram · 3 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 43 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- algorithm confusion
- A bug in which a verifier takes the algorithm from the token and uses one key for every algorithm, so that a public key can serve as an HMAC secret. A key that is for one algorithm, and a list of allowed algorithms, prevent it.
- JWKS
- JSON Web Key Set: a list of public keys, each with a key ID, that an issuer publishes at a URL so that services can check its signatures. Services keep a copy for a while, and a rotation adds and removes keys in it.
- JWT
- JSON Web Token (RFC 7519): three base64url parts joined by dots, a header, a payload of claims and a signature. The payload is encoded and not encrypted, so anyone who holds a token can read it.
- token introspection
- A call in which a service asks the issuer whether a token is still active and what it allows (RFC 7662). It makes revocation immediate at the cost of a call, which services often cache for a short time.
References
- RFC 8725: JSON Web Token Best Current Practices (IETF (RFC Editor))
- RFC 7519: JSON Web Token (JWT) (IETF (RFC Editor))
- RFC 7515: JSON Web Signature (JWS), section 4.1 header parameters (IETF (RFC Editor))
- RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (IETF (RFC Editor))
- RFC 7662: OAuth 2.0 Token Introspection (IETF (RFC Editor))
- RFC 10017: OAuth 2.0 for Browser-Based Applications (sections 6.1 and 8) (IETF (RFC Editor))
- RFC 9700: Best Current Practice for OAuth 2.0 Security (section 2.2.2) (IETF (RFC Editor))
- ASVS 5.0.0, V9 Self-contained Tokens (9.1.1 to 9.2.4) (OWASP Foundation)
- ASVS 5.0.0, V10 OAuth and OIDC (10.4.5, 10.4.8) (OWASP Foundation)
- JSON Web Token Cheat Sheet (OWASP Foundation)
- NIST SP 800-63B-4, section 5: Session Management (NIST)
- Web Crypto API (Node.js v24 documentation) (OpenJS Foundation (Node.js))
- jose, JSON Web Almost Everything (createRemoteJWKSet and jwtVerify) (jose project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress