How JWT Works: Structure, Signing, and Verification
Abhay Khant
Jan 1, 1970 • 6 min read
How JWT Works: Structure, Signing, and Verification Explained
- A JWT packs three base64url parts: header, payload, and signature, joined by dots
- The signature proves the payload was issued by whoever holds the secret or private key
- JWTs are signed, never encrypted: anyone holding the token can read the payload
- Expiration claims, strong secrets, and pinned algorithms stop the common attacks
The three parts of a JWT
A JSON Web Token is three text segments separated by dots, each segment being base64url-encoded JSON or bytes. The [JWT specification, RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519), defines the format, and the [visual introduction at jwt.io](https://jwt.io/introduction) remains a popular first stop, and every implementation from Node to Java produces the same shape:
header.payload.signature
The header names the signing algorithm and token type. The payload carries claims: statements about the user and the token itself. The signature is the part that makes it a credential rather than just a labeled note, and understanding how it is produced explains nearly everything about how JWTs behave in production.
A real token, built step by step
During the research for this guide we generated a working token with nothing but Python's standard library. The header encodes to eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, which decodes to {"alg":"HS256","typ":"JWT"}. The payload encodes to eyJzdWIiOiJ1c2VyLTEwNDIiLCJuYW1lIjoiU2FtIFJpdmVyYSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3NTg1MDAwMDAsImV4cCI6MTc1ODUwMzYwMH0, which decodes to claims naming user 1042, an editor role, an issued-at time, and an expiration one hour later.
The signature came from computing HMAC-SHA256 over the first two segments joined by a dot, keyed with a shared secret, yielding hB9-WvpqnANgQokXHJnEbw6ZV4tJ2t-TwMwLXZ2eLWg. The complete token is the three segments joined with dots, and it is exactly what a login endpoint would hand a browser.
What the signature actually protects
Here is the property that makes the whole scheme work. We took that real token and changed one claim: the role flipped from editor to admin. Recomputing the HMAC over the modified payload produces a different signature than the one the token carries, so a server comparing them rejects the forgery immediately. Signing with the wrong secret produces the same rejection.
Verification is therefore a one-way gate. A server that receives a token recomputes the signature from the header and payload it received, using its own secret or the issuer's public key, and accepts the token only on an exact match. An attacker can read every claim, because the payload is merely encoded, but changing a single character breaks the signature. That is the entire security model, and it holds only as long as the secret stays secret.
The claims that matter
| Claim | Meaning |
|---|---|
| iss | Issuer: who created and signed the token |
| sub | Subject: the user or resource the token describes |
| aud | Audience: which service should accept this token |
| exp | Expiration time, after which the token is invalid |
| iat | Issued at: when the token was created |
| jti | Unique token ID, useful for revocation lists |
The [registered claims registry in RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1) standardizes these names, though servers must enforce exp and aud themselves; the signature proves who issued a token, not that you should still accept it. A structurally valid token from the wrong issuer, or one past its expiration, verifies cryptographically and should still be refused.
HS256 versus RS256
Two signing families dominate. HMAC-based HS256 uses one shared secret: every service that verifies tokens can also mint them, which is fine inside one backend and awkward across organizational boundaries. RSA- or ECDSA-based RS256 uses a key pair: the issuer signs with a private key, and anyone holding the published public key can verify without gaining the power to issue. Multi-service architectures almost always end up at RS256 for exactly that reason, with the [JWS specification, RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515) defining the signature container both use.
The attacks that keep recurring
- Weak secrets: HMAC security equals secret quality, and dictionary-crankable secrets like
secretfall in seconds - Algorithm confusion: servers that trust the token's own header have accepted tokens signed with a public key as if it were an HMAC secret
- The none algorithm: early libraries accepted unsigned tokens when the header said so; modern practice pins the expected algorithm server-side, as [RFC 8725, the JWT best-current-practice document](https://datatracker.ietf.org/doc/html/rfc8725), requires
- Missing expiration: tokens without
explive forever, so a single leak becomes a permanent breach
Every one of these is a server-side validation failure rather than a break in the cryptography. The signature math held in each case; the code around it skipped a check.
Where tokens should live in the browser
A JWT in browser storage is readable by any script on the page, so a single XSS bug exfiltrates the session. The [OWASP session management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) favors HttpOnly cookies for browser sessions precisely because JavaScript cannot read them, trading some CSRF exposure that SameSite attributes and CSRF tokens then cover. Web storage remains reasonable for short-lived API tokens in controlled single-page apps, a pattern the [MDN web storage documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API) documents with the same caveats. Cookie-based sessions also sidestep manual header wiring, since the [browser cookie mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) attaches them to requests automatically.
Inspecting tokens in daily work
Decoding is deliberately trivial, which is half the fun and half the danger. Paste any token into the JWT decoder to read its header, claims, and expiration without sending it anywhere; it is the fastest way to answer why-did-my-session-expire questions. For deeper JSON inspection, the JSON formatter handles the decoded payload, and our [guide to how JWT decoding works](/blog/decode-jwt-token-online/) walks through the process claim by claim.
Signed statements, not locked boxes
A JWT is a signed statement about a user: readable by anyone, trustworthy only when the signature verifies and the claims still hold. Choose the algorithm family that matches your architecture, enforce expiration and audience on every request, keep HMAC secrets long and random or move to RS256, and store browser tokens where scripts cannot read them. Do those five things and the token format earns the trust the industry has placed in it.


