Tools · Cryptography and passwords in practice · 17 / 20
JSON Web Tokens explained: structure, signing algorithms, and validating them correctly
Decode a JWT’s header, payload, and signature, choose HS256, RS256, or EdDSA, and validate tokens with a library that pins algorithms and checks expiry, audience, and issuer.
Three parts
A JWT is three base64url-encoded parts separated by dots: a header naming the algorithm (alg) and key ID (kid), a payload with claims such as sub, iss, aud, exp, and iat, and a signature over the first two parts. The payload is encoded, not encrypted: anyone holding the token can read it. Do not put secrets in it.
Algorithms
HS256 uses a shared secret, so every service that verifies can also mint tokens. RS256 and ES256 use a private key to sign and a public key to verify, which suits identity providers that publish keys through a JWKS endpoint. EdDSA (Ed25519) is a modern option where supported. The alg value none means no signature and must always be rejected.
Lab: decode and validate with PyJWT
Create a token with PyJWT, decode the parts by hand to see the plain JSON, and validate it properly by specifying allowed algorithms, audience, and issuer. Then try validation failures: an expired token, a wrong audience, and a token whose payload you edited without re-signing.
import jwt, time, base64, json
secret = 'lab-only-secret-with-enough-length-32b'
token = jwt.encode({'sub': 'alice', 'aud': 'orders-api', 'iss': 'https://idp.lab', 'exp': int(time.time()) + 300}, secret, algorithm='HS256')
header, payload, sig = token.split('.')
pad = lambda s: s + '=' * (-len(s) % 4)
print(json.loads(base64.urlsafe_b64decode(pad(header))), json.loads(base64.urlsafe_b64decode(pad(payload))))
claims = jwt.decode(token, secret, algorithms=['HS256'], audience='orders-api', issuer='https://idp.lab')
print(claims['sub'])
for bad in [
jwt.encode({'sub': 'alice', 'aud': 'orders-api', 'iss': 'https://idp.lab', 'exp': int(time.time()) - 10}, secret, algorithm='HS256'),
jwt.encode({'sub': 'alice', 'aud': 'billing', 'iss': 'https://idp.lab', 'exp': int(time.time()) + 300}, secret, algorithm='HS256'),
header + '.' + base64.urlsafe_b64encode(b'{"sub":"admin","aud":"orders-api","iss":"https://idp.lab"}').decode().rstrip('=') + '.' + sig,
]:
try:
jwt.decode(bad, secret, algorithms=['HS256'], audience='orders-api', issuer='https://idp.lab')
except jwt.PyJWTError as e:
print('rejected:', type(e).__name__)Validation rules
Pin the expected algorithms instead of trusting the header. Verify signature, exp, nbf, aud, and iss on every request. Fetch public keys from the issuer’s JWKS over HTTPS and match by kid, caching sensibly. Keep tokens short-lived and use refresh tokens for longer sessions. Store tokens in browsers carefully; HttpOnly cookies limit exposure to script injection.
Diagram
A JWT, part by part
Readable by anyone; trustworthy only after validation.
Header
{"alg":"RS256","kid":"2026-11","typ":"JWT"}Algorithm and key ID
Payload
{"sub":"alice","aud":"orders-api","iss":"https://idp.lab","exp":1793620000}Claims, not encrypted
Signature
base64url(sign(header.payload))
Integrity over both parts
Check yourself
1. Is a JWT payload confidential?
2. How should a verifier choose the algorithm?
3. Which alg value must always be rejected?