What a JWT is
A JSON Web Token (JWT, often pronounced "jot") is a compact, URL-safe string that carries a set of claims, such as who a user is and when the token expires, together with a signature that lets the receiver check the token has not been altered. JWTs are defined in RFC 7519 and are widely used for API authentication, single sign-on, and passing identity between services.
The key idea is that the server does not need to look anything up to trust a valid token. If the signature checks out with the right key, the claims inside are exactly what the issuer wrote. That makes JWTs convenient for distributed systems, and it is also the source of most JWT mistakes.
The three parts of a token
A JWT is three Base64url-encoded parts separated by dots: a header, a payload, and a signature. Here is a well-known example token, split onto lines for readability:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ <- payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <- signatureDecoding the first two parts gives plain JSON:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }The header names the signing algorithm. The payload holds the claims. The signature is computed over the encoded header and payload, so changing even one character in either part makes the signature invalid.
Base64url encoding is not encryption. Anyone who has a token can read its payload, so never put passwords, secrets, or sensitive personal data in a JWT.
Standard claims
RFC 7519 registers a handful of short claim names that most libraries understand. Time-based claims are Unix timestamps in seconds, not milliseconds.
| Claim | Name | Meaning |
|---|---|---|
iss | Issuer | Who created and signed the token |
sub | Subject | Who the token is about, usually a user ID |
aud | Audience | Which service the token is intended for |
exp | Expiration time | After this time the token must be rejected |
nbf | Not before | Before this time the token must be rejected |
iat | Issued at | When the token was created |
jti | JWT ID | A unique ID, useful for detecting replay or revoking a token |
Applications add their own claims too, such as roles, scopes, or a tenant ID. Keep custom claims small: tokens travel with every request, often in headers with size limits.
Signing algorithms
| Algorithm | Type | Keys |
|---|---|---|
| HS256, HS384, HS512 | HMAC with SHA-2 | One shared secret signs and verifies |
| RS256 | RSA signature | Private key signs, public key verifies |
| ES256 | ECDSA signature | Private key signs, public key verifies; smaller keys and signatures |
HMAC algorithms are simple and fast, but every service that verifies tokens must hold the same secret, and any of them could also mint new tokens. Asymmetric algorithms such as RS256 and ES256 let many services verify tokens with a public key while only the issuer holds the private key, which is why identity providers usually use them and publish their public keys at a JWKS endpoint.
If you use HMAC, the secret must be long and random. A short or guessable secret can be brute-forced offline from a single captured token.
Decoding versus verifying
Decoding a token just reads its contents, which is useful for debugging. In JavaScript, convert Base64url to Base64 and parse the JSON:
function decodeJwtPart(part) {
const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
return JSON.parse(atob(padded));
}
const [header, payload] = token.split('.');
console.log(decodeJwtPart(header), decodeJwtPart(payload));Verifying is what makes a token trustworthy: checking the signature with the expected key and algorithm, then checking the expiry, issuer, and audience. Always use a maintained library for this, and tell it exactly which algorithms to accept.
import jwt from 'jsonwebtoken';
const claims = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'https://auth.example.com',
audience: 'orders-api',
});Security pitfalls
- Trusting decoded data without verifying. Reading claims from an unverified token lets anyone impersonate anyone.
- **Accepting the
nonealgorithm.** Unsigned tokens must be rejected. Pin the allowed algorithms instead of trusting the header. - Algorithm confusion. If a server that expects RS256 also accepts HS256, an attacker may sign a token using the public key as an HMAC secret. Pinning algorithms prevents this.
- **Ignoring
exp,aud, oriss.** A correctly signed token for a different service, or one that expired last month, should still be rejected. - Long-lived tokens. A stolen token is valid until it expires. Keep access tokens short-lived, often minutes, and use refresh tokens to obtain new ones.
- No way to revoke. Because verification needs no database lookup, logging out does not invalidate an issued token. Short lifetimes, token IDs on a deny list, or rotating keys address this.
Where to store tokens in the browser
Tokens in localStorage are easy to use from JavaScript, which also means any cross-site scripting (XSS) bug can read and steal them. Tokens in HttpOnly cookies cannot be read by scripts, which protects against theft through XSS, but cookies are sent automatically, so you need cross-site request forgery (CSRF) protection, such as SameSite cookies and CSRF tokens.
For many web applications, an HttpOnly, Secure, SameSite cookie holding a short-lived token is the more robust default. Whatever you choose, preventing XSS in the first place matters more than the storage location.
When a JWT is the right tool
JWTs shine when several independent services need to trust the same identity without sharing a session database, and when tokens are short-lived. For a single web application with one backend, a traditional server-side session identified by a cookie is often simpler and easier to revoke. Choose JWTs for the problem they solve, not because they are common.
To inspect a token while debugging, paste it into a JWT inspector that runs in your browser, such as the one in UtiliStudio Developer Utilities. Use test tokens where possible, and treat real production tokens like passwords.
Keep learning
- CORS errors explained, and how to fix them
- HTTP status codes explained: a practical reference
- REST Client: Send HTTP requests, inspect responses and test APIs without leaving UtiliStudio.