How to Secure Your API with JWT Authentication
JSON Web Tokens (JWTs) are a compact, self-contained way to transmit authentication information between a client and a server. Once issued, a JWT carries all the information the server needs to verify a user's identity — no database lookup required on every request. This tutorial walks you through implementing JWT authentication from scratch.
What Is a JWT?
A JWT is a Base64URL-encoded string made of three parts separated by dots: header.payload.signature
- Header: Specifies the token type (JWT) and the signing algorithm (e.g., HS256).
- Payload: Contains claims — data like user ID, roles, and expiry time.
- Signature: A cryptographic hash of the header and payload, signed with a secret key. This is what prevents tampering.
When a client sends a JWT in a request, the server re-computes the signature and compares it. If they match, the token is valid.
Step 1: Issue a Token at Login
When a user logs in with valid credentials, your server generates and returns a JWT:
const jwt = require('jsonwebtoken');
// After verifying username/password...
const payload = {
sub: user.id,
email: user.email,
roles: user.roles,
iat: Math.floor(Date.now() / 1000),
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m', // short-lived access token
});
res.json({ access_token: token });
Keep your JWT_SECRET in an environment variable — it should be a long, random string and never committed to source control.
Step 2: Send the Token in Requests
The client stores the token (typically in memory or a secure HTTP-only cookie) and sends it in the Authorization header on subsequent requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Step 3: Verify the Token in Middleware
Create an authentication middleware that validates the token before allowing access to protected routes:
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
// Apply to protected routes
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
Step 4: Implement Refresh Tokens
Access tokens should be short-lived (15 minutes is common). To avoid making users log in repeatedly, issue a long-lived refresh token alongside the access token. Store refresh tokens securely (in an HTTP-only cookie or your database) and create a /auth/refresh endpoint that issues a new access token in exchange for a valid refresh token.
Security Best Practices
- Never store JWTs in localStorage — it's vulnerable to XSS attacks. Prefer HTTP-only cookies or in-memory storage.
- Set short expiry times on access tokens. Rotate refresh tokens on each use.
- Use HTTPS exclusively — JWTs sent over plain HTTP can be intercepted.
- Validate all claims — check
exp(expiry),iss(issuer), andaud(audience) as appropriate. - Implement a token blocklist if you need immediate revocation capability (e.g., on logout).
Common Pitfalls to Avoid
- Using a weak or hardcoded
JWT_SECRET - Putting sensitive data (passwords, PII) in the payload — remember, the payload is only Base64-encoded, not encrypted
- Setting excessively long expiry times on access tokens
- Skipping signature verification and trusting the payload blindly
JWT authentication, when implemented correctly, provides a stateless, scalable security layer for your API. Combine it with HTTPS, proper secret management, and refresh token rotation for a production-ready authentication system.