JSON Web Tokens (JWT) are a widely-used, stateless authentication mechanism for APIs — letting your server verify a client's identity without needing a database lookup on every request.
How JWT Authentication Works
- User logs in with credentials
- Server verifies credentials, issues a signed JWT containing user identity/claims
- Client includes this token in subsequent requests (typically an
Authorizationheader) - Server verifies the token's signature on each request — no database lookup needed to confirm authenticity
JWT Structure
A JWT has three parts separated by dots: header.payload.signature. The header and payload are base64-encoded JSON (readable, not encrypted); the signature proves the token wasn't tampered with.
Implementing JWT Issuance (Node.js Example)
const jwt = require('jsonwebtoken');
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
Verifying Tokens on Protected Routes
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: 'No token provided' });
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
app.get('/protected', authenticate, (req, res) => {
res.json({ message: `Hello, user ${req.user.userId}` });
});
Choosing an Appropriate Expiration Time
Short-lived tokens (minutes to a few hours) limit the impact if a token is ever stolen, at the cost of requiring more frequent re-authentication or refresh — balance based on your application's security requirements.
Implementing Refresh Tokens (For Longer Sessions)
Rather than issuing long-lived access tokens, issue a short-lived access token plus a longer-lived refresh token, stored securely, used only to obtain new access tokens:
const accessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, process.env.REFRESH_SECRET, { expiresIn: '7d' });
// Store refreshToken securely (httpOnly cookie or database), never in localStorage
Where to Store the Token on the Client
| Storage Method | Risk |
|---|---|
| localStorage | Vulnerable to XSS attacks — any injected script can read it |
| httpOnly cookie | Protected from XSS reading, but requires CSRF protection |
For most web applications, an httpOnly, secure cookie is the safer choice over localStorage.
Critical Security Practices
- Use a strong, randomly generated secret for signing — never a guessable string
- Store the signing secret as an environment variable, never in code — see How to Manage Environment Variables and Secrets on a VPS
- Always set an expiration (
expiresIn) — never issue tokens that never expire - Use HTTPS everywhere — tokens transmitted over plain HTTP can be intercepted
Handling Token Revocation
JWTs are stateless by design, meaning there's no built-in way to revoke a specific token before its natural expiration. For scenarios requiring revocation (logout, compromised account), maintain a server-side blocklist of revoked token IDs, checked on each request — a partial trade-off against pure statelessness.
Common Errors
"jwt malformed" — the token wasn't correctly extracted from the header, or was corrupted in transit; verify the exact header format matches what your verification code expects.
Token verification fails despite a valid-looking token — confirm the signing secret used to verify matches exactly what was used to issue it (common issue when secrets differ between environments).
Best Practices
- Keep access tokens short-lived; use refresh tokens for longer sessions
- Never store the JWT secret in version control
- Prefer httpOnly cookies over localStorage for web clients
Continue Reading
- How to Build and Secure a REST API on a VPS
- How to Manage Environment Variables and Secrets on a VPS
- How to Rate Limit an API with Nginx
Browse more articles in Object Storage, Messaging & APIs.
