Unit 7: Logins, MFA and tokens · Lesson 6 of 7 · 22 min
Reading JWTs
Decode a token, read its claims and check that the API enforces them.
mission
- You are
- The analyst reviewing the shop's API sign-in.
- Situation
- The shop's mobile app gets a token from
/api/tokenand sends it to the API. Tokens are meant to expire after 15 minutes. - Goal
- Decode the token, read its claims and test whether the API respects the expiry.
- Hand in
- The token's claims and a finding if expiry is not enforced.
A JWT
- 1
eyJhbGci...header: algorithm and type - 2
eyJpc3Mi...payload: the claims, readable by anyone - 3
d08sbwAE...signature: proves the server issued it
Get a token
curl -s -d @shop-account.txt http://127.0.0.1:8000/api/token | jq . { "token_type": "Bearer", "expires_in": 900, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Nob3AuaW50ZXJuYWwiLCJzdWIiOiJsZWFybmVyIiwicm9sZSI6ImN1c3RvbWVyIiwiaWF0IjoxNzkxOTY4NDAwLCJleHAiOjE3OTE5NjkzMDB9.d08sbwAEhs0DgnBier-XpQ_DfyGE8sFcvvl7UTcpomo" } curl -s -d @shop-account.txt http://127.0.0.1:8000/api/token | jq -r .access_token > token.txt
you should see A bearer token valid for 900 seconds.
Decode the header and payload
The parts are base64url, not encrypted. Anyone holding the token can read them.
cut -d. -f1 token.txt | jq -R '@base64d | fromjson' { "alg": "HS256", "typ": "JWT" } cut -d. -f2 token.txt | jq -R '@base64d | fromjson' { "iss": "https://shop.internal", "sub": "learner", "role": "customer", "iat": 1791968400, "exp": 1791969300 }
you should see HS256, then issuer, subject, role and two timestamps.
defender tip Never put secrets or personal data in a JWT payload. Signing stops changes, not reading.
When does it expire?
cut -d. -f2 token.txt | jq -R '@base64d | fromjson | .exp | todate'
"2026-10-14T09:15:00Z"
you should see It expired at 09:15 on 14 October 2026, fifteen minutes after it was issued and long before today.
Does the API care?
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $(cat token.txt)" http://127.0.0.1:8000/api/v1/me
200
you should see 200. The API checks the signature but not exp, so a leaked token works forever.
Token checks
claims = verify_signature(token)
Expired tokens still work.
claims = verify_signature(token) if claims["exp"] < now(): reject() if claims["iss"] != ISSUER: reject()
Check expiry, issuer and audience on every request.
What a defender takes away
- 1A JWT's payload is readable by anyone who holds it.
- 2APIs must check signature, expiry, issuer and audience.
- 3Short expiry only helps if the server enforces it.
Questions
0 of 3 solved
Answer each one from what you find on your lab machine.