What Is a JWT Token?
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format used to securely transmit information between two parties. Defined by RFC 7519, JWTs are the backbone of modern authentication and authorisation flows — from single-page apps calling REST APIs to microservices verifying each other's identity.
Unlike opaque session tokens that require a server-side lookup, JWTs are self-contained: everything needed to validate the token and extract user information is baked right into the token itself. This makes them stateless, scalable, and ideal for distributed systems.
JWT Structure: Header, Payload, and Signature
Every JWT consists of three Base64URL-encoded parts separated by dots:
header.payload.signature
// Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c1. Header
The header is a JSON object that describes the token type and the signing algorithm. It is Base64URL-encoded to form the first segment.
{
"alg": "HS256",
"typ": "JWT"
}2. Payload (Claims)
The payload carries the claims — statements about the user and additional metadata. Claims come in three flavours:
- Registered claims: Standardised fields like
iss,exp,sub, andiat. - Public claims: Custom fields registered in the IANA JSON Web Token Claims registry to avoid collisions.
- Private claims: Application-specific fields agreed upon between parties (e.g.,
role,orgId).
{
"sub": "user_4821",
"name": "Jane Smith",
"role": "admin",
"iss": "https://auth.example.com",
"iat": 1716163200,
"exp": 1716166800
}3. Signature
The signature is created by taking the encoded header and payload, concatenating them with a dot, and signing the result using the algorithm specified in the header.
// HMAC-SHA256 example
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)Base64URL encoding is slightly different from standard Base64: it replaces + with-, / with _, and strips trailing = padding. This makes the token safe for URLs, headers, and cookies.
Signing Algorithms: HS256 vs RS256
The signing algorithm is arguably the most critical decision when working with JWTs. The two most common algorithms are:
| Algorithm | Type | Key | Best For |
|---|---|---|---|
| HS256 | Symmetric (HMAC) | Shared secret | Single-service apps where issuer and verifier are the same |
| RS256 | Asymmetric (RSA) | Private key signs, public key verifies | Microservices, third-party integrations, OIDC providers |
| ES256 | Asymmetric (ECDSA) | Smaller keys, faster operations | Mobile apps, IoT, performance-sensitive systems |
With HS256, the same secret is used to sign and verify the token. This is simple but means every service that needs to verify tokens must have access to the secret. With RS256, only the auth server holds the private key; any service can verify tokens using the freely distributed public key.
Never accept tokens where alg is set to "none". This is a well-known attack vector where an attacker removes the signature entirely. Always validate the algorithm against an explicit allowlist on the server.
Common JWT Claims Explained
The registered claims defined in RFC 7519 provide a shared vocabulary. Here are the most important ones:
| Claim | Full Name | Description |
|---|---|---|
iss | Issuer | Who issued the token (e.g., your auth server URL) |
sub | Subject | The principal the token represents (usually a user ID) |
aud | Audience | Intended recipient(s) of the token (e.g., an API identifier) |
exp | Expiration Time | Unix timestamp after which the token is invalid |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Token is not valid before this Unix timestamp |
jti | JWT ID | Unique identifier for the token (useful for revocation) |
The Verification Process
When your server receives a JWT, it must verify the token before trusting its contents. Here is the standard verification flow:
Parse the Token
Split the token by dots into header, payload, and signature segments.
Decode the Header
Base64URL-decode the header and confirm that the alg field matches your expected algorithm. Reject tokens with "alg": "none" or unexpected algorithms.
Verify the Signature
Recompute the signature using the header and payload with your key (secret or public key). Compare it to the signature in the token. If they don't match, reject the token immediately.
Validate the Claims
Check exp (not expired), nbf (not used too early), iss(trusted issuer), and aud (intended for your service).
// Node.js verification with jsonwebtoken
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // explicit allowlist
issuer: 'https://auth.example.com',
audience: 'my-api',
});
console.log('User ID:', decoded.sub);
} catch (err) {
console.error('Token verification failed:', err.message);
}Common Security Mistakes
JWTs are secure by design, but misusing them can introduce serious vulnerabilities. Watch out for these common pitfalls:
- Storing tokens in localStorage: This exposes tokens to cross-site scripting (XSS) attacks. Prefer
HttpOnly,Secure,SameSitecookies instead. - Not validating the algorithm: An attacker can change the header to
"alg": "none"or switch from RS256 to HS256 (using the public key as the HMAC secret). Always enforce the algorithm server-side. - Overly long expiration times: Tokens valid for days or weeks increase the window of compromise. Use short-lived access tokens (5–15 minutes) paired with refresh tokens.
- Sensitive data in the payload: Remember, the payload is only encoded, not encrypted. Anyone can decode it. Never put passwords, credit card numbers, or secrets in the payload.
- Ignoring token revocation: JWTs are stateless, making revocation non-trivial. Use short expiration times, token blacklists, or the
jticlaim for revocation scenarios.
Use a dedicated JWT debugger (like jwt.io or a local decoding tool) during development to inspect token contents. Paste the token and immediately see the decoded header, payload, and signature verification status.
Debugging JWT Issues
When a JWT-based flow breaks, here's a systematic approach to tracking down the problem:
- Decode the token: Paste it into a JWT decoder to inspect the header and payload. Verify the claims look correct.
- Check expiration: Convert the
expclaim to a human-readable date. Clock skew between servers is a common cause of premature expiration. - Verify the issuer and audience: Mismatched
issoraudvalues cause silent rejections in many libraries. - Confirm the signing key: For HS256, ensure the secret matches exactly (including encoding). For RS256, verify you're using the correct public key or JWKS endpoint.
- Inspect the HTTP headers: Confirm the token is being sent as
Authorization: Bearer <token>and not accidentally truncated.
Conclusion
JWTs are a powerful, standardised way to handle authentication and authorisation in modern applications. Their self-contained nature makes them perfect for stateless architectures, but that same power demands careful implementation. By understanding the structure, choosing the right algorithm, validating claims rigorously, and avoiding common security pitfalls, you can build JWT-based systems that are both secure and scalable.
When in doubt, lean on well-maintained libraries for signing and verification — don't roll your own crypto. And always keep a JWT debugger handy during development; it will save you hours of frustration.
🎯 Key Takeaways
- JWTs have three parts: a Base64URL-encoded header, payload, and signature separated by dots.
- Use HS256 for single-service setups and RS256/ES256 for distributed or third-party systems.
- Always validate
alg,exp,iss, andaudserver-side. - Never store sensitive data in the payload — it is encoded, not encrypted.
- Store tokens in HttpOnly cookies, not localStorage, to mitigate XSS attacks.
- Use short-lived access tokens (5–15 min) with refresh tokens for a secure token lifecycle.