A JSON Web Token (JWT, RFC 7519) is a compact, URL-safe string that carries claims — statements about a user or session — between two parties, most often to prove that someone is logged in. Its anatomy fits on one line: header.payload.signature, three base64url-encoded segments joined by dots.
The three segments: header, payload, signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| Segment | Contains | Decoded value |
|---|---|---|
eyJhbGci... — header | Token metadata: signing algorithm and type. | {"alg":"HS256","typ":"JWT"} |
eyJzdWIi... — payload | The claims — the data the token asserts. | {"sub":"1234567890","name":"John Doe","iat":1516239022} |
SflKxwRJ... — signature | HMAC-SHA256 of the first two segments, made by the issuer. | Raw bytes, not JSON. |
Header and payload are ordinary JSON once decoded. The signature is different: a cryptographic function over the exact string header.payload, so changing one character invalidates it — that is what makes a JWT tamper-evident, provided the verifier checks the signature.
Base64url is encoding, not encryption
Each segment uses base64url, the URL-safe Base64 variant from RFC 4648. Plain Base64 maps binary data onto A-Z, a-z, 0-9, plus + and /, padded with = — awkward characters in URLs and HTTP headers. Base64url swaps + for -, / for _, and omits padding: the bytes 0xFB 0xFF encode as +/8= in standard Base64 and as -_8 in base64url.
It is an encoding, not a cipher: anyone can reverse it with no key, so never put secrets such as passwords or API keys into a token.
The standard claims
RFC 7519 registers seven claim names. All are optional, but exp, iss and aud appear in almost every well-behaved token. Timestamps use NumericDate: seconds since the Unix epoch, not milliseconds.
| Claim | Stands for | Meaning | Example |
|---|---|---|---|
iss | Issuer | Who created and signed the token, usually a URL. | "https://auth.example.com" |
sub | Subject | Who the token is about, typically a user ID. | "user-42" |
aud | Audience | Who the token is for; a string or an array of strings. A server must reject a token whose aud does not name it. | "billing-api" |
exp | Expiration time | The token must not be accepted at or after this time. | 1757003600 |
iat | Issued at | When the token was created; used to compute its age. | 1757000000 |
nbf | Not before | The token must not be accepted before this time. | 1757000000 |
jti | JWT ID | A unique token identifier, used to detect replays or drive a revocation list. | "d8f7c2a1" |
In these examples exp is exactly 3600 seconds after iat: a one-hour token. The iat 1516239022 in the sample token is 2018-01-18 01:30:22 UTC. A 13-digit value like 1516239022000 is milliseconds; read as seconds it lands tens of thousands of years in the future.
Decoding is not verifying
Reading a JWT requires no secret, so decoding says nothing about whether the token is genuine. Two classic attacks exploit verifiers that forget this:
- The alg-none attack. RFC 7518 defines an unsecured JWT with header
{"alg":"none","typ":"JWT"}and an empty signature segment (the token ends with a trailing dot). Early libraries accepted such tokens as valid, letting anyone forge arbitrary claims. Modern libraries reject it unless explicitly enabled; pin the expected algorithm in configuration rather than trusting the header. - Weak-secret attacks on HS256. HS256 signs with one shared secret, and anyone holding a token can attack it offline: compute HMAC-SHA256 of
header.payloadwith candidate passwords until the signature matches. Tools like hashcat test millions of guesses per second on one GPU, so a secret like"secret"falls instantly, after which the attacker can mint valid tokens with any claims. Use at least 256 bits of cryptographically random secret, or move to RS256 or ES256, where a private key signs and a public key verifies.
Verifying properly means: check the signature with the right key, confirm the algorithm is the one you expect, then validate exp, nbf, iss and aud. Until all of that has happened on your server, treat decoded claims as untrusted input.
Safe debugging practices
- Treat tokens like passwords. A bearer token grants access until
exp, so never paste production tokens into random websites, commit them to git, or write them to logs. - Strip the Bearer prefix. Tokens copied from an Authorization header start with
"Bearer ", which is not part of the JWT and breaks parsing. - Read timestamps as seconds. NumericDate values are seconds since the epoch; a 13-digit number is milliseconds.
- Redact before sharing. In bug reports and screenshots, share the decoded claims, not the raw token.
Try it: paste any JWT into the JWT Decoder — it runs entirely in your browser, so the token never leaves your device. The tool pretty-prints the header and payload, renders exp, iat and nbf as human-readable UTC times with an Expired or Valid badge, and flags that the signature is not verified. No token at hand? Click Sample token to generate one with fresh timestamps.