How JWT Works: Structure, Signing, and Verification
A JSON Web Token (JWT) is a compact, URL-safe string made of three base64url-encoded parts separated by dots: a header, a payload, and a signature. The signature proves the token has not been tampered with — but the payload is not encrypted and can be read by anyone who holds the token.
Want to inspect a JWT right now? Paste any token into Jsonic's JWT Decoder to see the decoded header, payload claims, and expiration time instantly.
Decode a JWTJWT anatomy: the 3-part structure
Every JWT looks like this — three base64url strings joined by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cSplit on the dots and you get three segments:
| Part | Content | Example (decoded) |
|---|---|---|
| Header | Algorithm and token type | {"alg":"HS256","typ":"JWT"} |
| Payload | Claims (user data + metadata) | {"sub":"1234567890","exp":1516242622} |
| Signature | HMAC or RSA digest | Binary bytes, base64url-encoded |
The formula is: base64url(header) + "." + base64url(payload) + "." + base64url(signature). Neither the header nor the payload is encrypted — base64url is just an encoding, not encryption. The signature is what makes the token trustworthy.
Header and algorithm
The header is a small JSON object. It tells the verifier which algorithm was used to create the signature. The alg field is critical — the verifier uses it to pick the right key and hash function.
{
"alg": "HS256",
"typ": "JWT"
}Common alg values are HS256, RS256, and ES256. The typ field is always "JWT" for a standard JSON Web Token. Some libraries also include a kid (key ID) field when the server rotates signing keys, so verifiers know which key to use.
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-key-1"
}Payload and standard claims
The payload is a JSON object containing claims — statements about the subject (usually a user) plus metadata about the token itself. The IANA JWT registry defines a set of standard (registered) claim names:
| Claim | Full name | Meaning |
|---|---|---|
iss | Issuer | Who created and signed the token (e.g. "https://auth.example.com") |
sub | Subject | Who the token is about — typically a user ID |
aud | Audience | Intended recipient(s) — the verifier must match this value |
exp | Expiration | Unix timestamp after which the token must be rejected |
iat | Issued at | Unix timestamp when the token was created |
jti | JWT ID | Unique identifier — used to prevent replay attacks |
A typical payload looks like this. Custom claims (like role and name) are perfectly valid alongside registered claims:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"iat": 1516239022,
"exp": 1516242622
}The exp and iat values are seconds since the Unix epoch (not milliseconds). A one-hour token has exp = iat + 3600.
Signature: how it works
The signature is computed from the encoded header and encoded payload using a cryptographic function and a secret or private key. For HS256 (HMAC with SHA-256):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)The result is a 256-bit (32-byte) digest, which is then base64url-encoded and appended as the third part of the token.
For RS256 (RSA with SHA-256), the process is similar but asymmetric:
// Signing (server only, uses private key)
signature = RSA_SHA256_sign(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
// Verifying (any party with the public key)
valid = RSA_SHA256_verify(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
signature,
publicKey
)Because the signature covers both the header and payload, any change to either part — even a single character — produces a completely different signature. A verifier immediately detects tampering by recomputing the expected signature and comparing it to the received one.
HS256 vs RS256 vs ES256
The three most common JWT signing algorithms differ in the type of key they use and the scenarios they fit best:
| Algorithm | Type | Key | Use when |
|---|---|---|---|
HS256 | Symmetric HMAC-SHA256 | Single shared secret | Single service or server-to-server where the signer and verifier are the same party |
RS256 | Asymmetric RSA-SHA256 | Private key (sign) + Public key (verify) | Multiple services need to verify tokens — share only the public key |
ES256 | Asymmetric ECDSA-SHA256 | EC private key (sign) + EC public key (verify) | Same as RS256 but produces a smaller signature — better for mobile and bandwidth-constrained clients |
HS256 is simple to set up but every service that verifies tokens must hold the secret. A compromised verifier exposes the signing key. RS256 and ES256 eliminate that risk: the private key never leaves the auth server, and public keys can be distributed freely via a JWKS (JSON Web Key Set) endpoint.
How JWT authentication works
JWTs are most commonly used in the Authorization header to authenticate API requests. Here is the complete flow:
// 1. Client logs in with credentials
POST /auth/login
{ "email": "user@example.com", "password": "hunter2" }
// 2. Server verifies credentials, issues a JWT
HTTP 200 OK
{ "token": "eyJhbGci..." }
// 3. Client stores the token (memory or HttpOnly cookie)
// 4. Client sends the token with every protected request
GET /api/profile
Authorization: Bearer eyJhbGci...
// 5. Server verifies the token without a database lookup:
// a) Re-compute signature from header + payload + secret
// b) Compare computed signature to received signature
// c) Check exp > now
// d) Check iss and aud match expected values
// e) If all pass → request is authenticatedThe key advantage of JWTs is statelessness: the server does not need to store session data. All the information needed to authenticate a request is self-contained in the token. This makes JWTs horizontally scalable — any server instance can verify any token without a shared session store.
Step-by-step token verification
When your server receives a JWT, the verification process follows these exact steps:
// Node.js example using the jsonwebtoken library
import jwt from 'jsonwebtoken'
function verifyToken(token: string, secret: string) {
// Steps 1–5 happen inside jwt.verify():
// 1. Split token into header, payload, signature
// 2. base64url-decode header → read "alg"
// 3. Re-compute signature: HMACSHA256(header + "." + payload, secret)
// 4. Compare computed signature to received signature (timing-safe)
// 5. Parse payload, check exp, iss, aud
try {
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'], // whitelist algorithms
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
})
return decoded // { sub, name, role, iat, exp, ... }
} catch (err) {
// TokenExpiredError, JsonWebTokenError, NotBeforeError
throw new Error('Invalid or expired token')
}
}Always whitelist the algorithms option. Without it, a JWT with "alg": "none" in the header can bypass signature verification on vulnerable libraries.
Common JWT mistakes
| Mistake | Why it matters | Fix |
|---|---|---|
| Storing sensitive data in the payload | Payload is base64url, not encrypted — anyone can read it | Store only non-sensitive identifiers (user ID, role). Never store passwords or PII. |
Not checking exp | Expired tokens remain accepted indefinitely | Always verify exp — use a library that does this automatically |
| Using a weak or hardcoded secret | Brute-force or leaked secrets allow token forgery | Use at least 32 random bytes for HS256 secrets; rotate secrets periodically |
Not whitelisting alg | "alg":"none" attack bypasses signature check | Always pass an explicit algorithms list to your JWT library |
Storing JWT in localStorage | Accessible to any script — vulnerable to XSS | Use memory or an HttpOnly cookie with SameSite=Strict |
| Very long token TTL without refresh | Stolen tokens remain valid for hours or days | Use short-lived access tokens (15 min) with refresh tokens for re-issue |
Frequently asked questions
Is the JWT payload encrypted?
No. The payload is base64url encoded, not encrypted. Base64url is a reversible encoding — anyone who holds the token can decode and read the claims. Never put passwords, credit card numbers, or other sensitive data in the payload. If you need a confidential payload, use JWE (JSON Web Encryption) instead of a plain JWT.
How does a server verify a JWT without storing it?
The server recomputes the signature from the received header and payload using its own secret (HS256) or public key (RS256). If the recomputed signature matches the one in the token, the token is authentic. The server then checks exp, iss, and aud. No database lookup is needed — all the information is self-contained in the token.
What is the difference between HS256 and RS256?
HS256 is symmetric: a single shared secret both signs and verifies tokens. Every service that verifies tokens must hold that secret. RS256 is asymmetric: a private key signs the token and a public key verifies it. The private key never leaves the auth server, and public keys can be published freely. RS256 is the better choice when multiple independent services need to verify tokens.
How do I check if a JWT has expired?
Decode the payload by base64url-decoding the middle segment, then read the exp claim. It is a Unix timestamp in seconds. In JavaScript:
const payload = JSON.parse(atob(token.split('.')[1]))
const isExpired = Date.now() / 1000 > payload.expMost JWT libraries (jsonwebtoken, jose, python-jwt) perform this check automatically inside their verify function and throw a TokenExpiredError when the token has expired.
Where should I store a JWT in the browser?
The two safest options are in-memory (a JavaScript variable or React state) and an HttpOnly cookie. In-memory storage is invisible to scripts and immune to XSS, but the token is lost on page refresh. An HttpOnly cookie survives refreshes and is inaccessible to JavaScript, but requires CSRF protection (use SameSite=Strict or a CSRF token). Avoid localStorage — it is readable by any script on the page and is a common XSS target.
What happens if the JWT secret is compromised?
With a compromised HS256 secret, an attacker can forge valid tokens for any user or role. Rotate the secret immediately — all existing tokens signed with the old secret become invalid, which will log out all active users. For RS256, revoke the private key and publish a new public key via your JWKS endpoint. Consider reducing the token TTL or implementing a token blocklist during the rotation window.
Inspect a JWT token
Paste any JWT into Jsonic's JWT Decoder to decode the header and payload, read all claims, and check the expiration time in human-readable format.
Open JWT Decoder