JWT Authentication: Implementation Guide
Learn how to implement secure JWT authentication from scratch. This comprehensive guide covers backend setup, token generation, validation, and best practices for production applications.
What is JWT Authentication?
JWT (JSON Web Token) authentication is a stateless authentication mechanism where the server generates a signed token after verifying user credentials. The client stores this token and sends it with subsequent requests to prove authentication.
Why JWT?
- Stateless - no server-side session storage needed
- Scalable - works seamlessly with load balancers
- Cross-domain - perfect for microservices and APIs
- Mobile-friendly - easy token storage in mobile apps
JWT Authentication Flow
1. POST /api/login { username, password }
2. Server: Verify credentials against database
3. Server: Generate signed JWT with user info
4. Response: { "token": "eyJhbGciOiJIUzI1NiIs..." }
5. Client: Store in localStorage/cookie
6. GET /api/user Authorization: Bearer <token>
7. Server: Verify signature + expiration
8. Response: Protected resource data
Backend Implementation (Node.js)
1. Install Dependencies
npm install jsonwebtoken bcrypt express
2. Generate JWT Secret
// Use a strong random secret (256-bit)
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
// Store in .env file: JWT_SECRET=your_secret_here
3. User Login Endpoint
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Find user in database
const user = await User.findOne({ username });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// Verify password
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
// Generate JWT
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'your-app' }
);
res.json({ token });
});
4. Authentication Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) return res.status(401).json({ error: 'Token required' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user; // Attach user info to request
next();
});
}
// Use in routes
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
Frontend Implementation (JavaScript)
Login Function
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) throw new Error('Login failed');
const { token } = await response.json();
localStorage.setItem('token', token);
return token;
}
Authenticated API Calls
async function fetchProtectedData() {
const token = localStorage.getItem('token');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Token expired or invalid - redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
return;
}
return await response.json();
}
Refresh Token Pattern
For better security, implement refresh tokens alongside short-lived access tokens:
| Token Type | Lifetime | Storage | Purpose |
|---|---|---|---|
| Access Token | 15 min - 1 hour | Memory/localStorage | API authentication |
| Refresh Token | 7-30 days | httpOnly cookie | Get new access tokens |
// Login returns both tokens
app.post('/api/login', async (req, res) => {
// ... validate credentials ...
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });
// Store refresh token in database
await RefreshToken.create({ userId: user.id, token: refreshToken });
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});
// Refresh endpoint
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.cookies;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
// Verify refresh token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// Check if token exists in database
const exists = await RefreshToken.findOne({ token: refreshToken });
if (!exists) return res.status(403).json({ error: 'Invalid refresh token' });
// Generate new access token
const accessToken = jwt.sign({ userId: decoded.userId }, SECRET, { expiresIn: '15m' });
res.json({ accessToken });
});
Security Best Practices
Critical Security Rules:
- Use HTTPS only - tokens transmitted over plain HTTP can be intercepted
- Strong secret: Minimum 256-bit random secret
- Short expiration: 15 min to 1 hour for access tokens
- Validate everything: Signature, expiration, issuer, audience
- HttpOnly cookies: For refresh tokens to prevent XSS
- Never log tokens: Tokens are sensitive credentials
Common Pitfalls to Avoid
- Storing sensitive data in JWT: JWTs are Base64-encoded, not encrypted. Don't put passwords or credit cards in tokens.
- No expiration time: Always set
expclaim. Tokens without expiration never expire. - Weak secrets: Don't use "secret", "password", or short strings.
- Client-side validation only: Always verify tokens on the server; client checks are for UX only.
- Not handling expired tokens: Implement refresh token logic or redirect to login.
Testing Your Implementation
# Test login
curl -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password123"}'
# Test protected route
curl http://localhost:3000/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6..."
Try Our Tools
Debug and test your JWT tokens: