A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe string that carries a set of claims, such as who the user is and when the token expires, plus a signature that proves the claims haven't been altered. JWTs are everywhere in modern authentication: OAuth 2.0 access tokens, OpenID Connect ID tokens, and session tokens for APIs. This guide shows how to read one and the security mistakes to avoid.
A JWT is three base64url-encoded segments separated by dots:
texteyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBc2hhIiwiaWF0IjoxNzczNTg1MDAwLCJleHAiOjE3NzM1ODg2MDB9.<signature>
The header and payload are just JSON encoded with base64url (Base64 with - and _ instead of + and /, and no padding). Decoding them needs no key:
Header:
json{ "alg": "HS256", "typ": "JWT" }
Payload:
json{ "sub": "42", "name": "Asha", "iat": 1773585000, "exp": 1773588600 }
In JavaScript:
javascriptfunction decodeJwtPart(part) { const base64 = part.replace(/-/g, '+').replace(/_/g, '/'); const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '='); const bytes = Uint8Array.from(atob(padded), c => c.charCodeAt(0)); return JSON.parse(new TextDecoder().decode(bytes)); } const [header, payload] = token.split('.').slice(0, 2).map(decodeJwtPart);
You can also paste a segment into the Base64 decoder after swapping - for + and _ for /, then format the result in the JSON Formatter. See How Base64 encoding works for the details.
| Claim | Meaning |
|---|---|
iss | Issuer: who created the token |
sub | Subject: usually the user ID |
aud | Audience: which service the token is for |
exp | Expiry time, in seconds since 1970 (not milliseconds) |
iat | Issued-at time |
nbf | Not valid before this time |
jti | Unique token ID, useful for revocation |
Because exp is in seconds, compare it with Date.now() / 1000 in JavaScript. Mixing up seconds and milliseconds is a classic bug; see Dates and times in JSON.
A standard JWT (a JWS) is signed, not encrypted. Anyone who has the token can read the payload. So:
Encrypted JWTs (JWE) exist, but they have five segments instead of three and are much less common.
Decoding is not verifying. A server must:
"alg": "none", and don't let the token's header choose between an HMAC secret and an RSA public key.exp and nbf, allowing a small clock skew of a minute or so.iss and aud, so tokens issued for another service are rejected.jose (JavaScript), PyJWT (Python) or the official SDK of your identity provider. Don't write signature checks by hand.Storing tokens in localStorage exposes them to any script running on the page, including cross-site scripting (XSS) attacks. An HttpOnly, Secure, SameSite cookie keeps the token out of JavaScript's reach. Many single-page apps keep short-lived access tokens in memory and use a refresh token in an HttpOnly cookie.