In this comprehensive tutorial, you'll learn how to implement JWT authentication in a Node.js application using the jsonwebtoken package and Express framework. We'll build a complete authentication system with login functionality and protected routes, demonstrating real-world JWT implementation patterns.
π 1. Environment Setup
Before we start building our JWT authentication system, let's set up the necessary dependencies and project structure.
Installing Required Packages
First, initialize your Node.js project and install the essential packages:
npm init -y
npm install express jsonwebtoken dotenv bcryptjs
npm install --save-dev nodemonHere's what each package does:
- express: Web framework for Node.js
- jsonwebtoken: Library for working with JSON Web Tokens
- dotenv: Loads environment variables from .env file
- bcryptjs: Library for hashing passwords
- nodemon: Development tool that automatically restarts the server
Project Structure
Create the following project structure:
jwt-auth-demo/
βββ .env
βββ package.json
βββ server.js
βββ middleware/
β βββ auth.js
βββ routes/
βββ auth.js
βββ protected.jsEnvironment Configuration
Create a .env file in your project root with a strong JWT secret:
# .env
JWT_SECRET=your_super_secure_jwt_secret_key_here
PORT=3000
NODE_ENV=developmentImportant: Use a strong, randomly generated secret key in production. You can use our JWTSecrets generator to create a secure key.
π€ 2. Login Interface (Token Issuance)
Let's create the core authentication logic starting with the login endpoint that issues JWT tokens.
Basic Server Setup
First, set up the basic Express server in server.js:
// server.js
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
// Mock user database (in production, use a real database)
const users = [
{
id: 1,
username: 'demo',
email: 'demo@example.com',
// Password: 'password123' (hashed)
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'
}
];
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Login Endpoint Implementation
Now, let's implement the login endpoint that validates credentials and issues JWT tokens:
// Add this to server.js
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({
error: 'Username and password are required'
});
}
// Find user in database
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
// Create JWT payload (don't include sensitive data)
const payload = {
id: user.id,
username: user.username,
email: user.email
};
// Sign the token
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'jwt-auth-demo',
audience: 'jwt-auth-demo-users'
}
);
// Return token and user info
res.json({
message: 'Login successful',
token,
user: {
id: user.id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Token Structure Explanation
The JWT token we create contains:
- Payload: User information (id, username, email)
- Expiration: 1 hour from issuance
- Issuer: Identifies who issued the token
- Audience: Identifies who the token is intended for
π‘οΈ 3. Protected Routes (Token Verification)
Now let's create middleware to protect routes by verifying JWT tokens.
Authentication Middleware
Create middleware/auth.js:
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
try {
// Get token from Authorization header
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Access denied. No token provided.'
});
}
// Extract token from "Bearer TOKEN" format
const token = authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
error: 'Access denied. Invalid token format.'
});
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user info to request object
req.user = decoded;
// Continue to next middleware/route handler
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired. Please login again.'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(403).json({
error: 'Invalid token.'
});
}
console.error('Auth middleware error:', error);
res.status(500).json({
error: 'Internal server error'
});
}
}
module.exports = authMiddleware;Advanced Middleware with Role-Based Access
For more complex applications, you might need role-based access control:
// middleware/auth.js (extended version)
function requireRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Authentication required'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
}
module.exports = { authMiddleware, requireRole };π‘ 4. Testing the Authentication System
Let's create protected routes and test our authentication system.
Protected Routes
Add these protected routes to your server.js:
// Add this to server.js
const authMiddleware = require('./middleware/auth');
// Protected route - User profile
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({
message: `Hello, ${req.user.username}!`,
user: req.user,
timestamp: new Date().toISOString()
});
});
// Protected route - Dashboard
app.get('/api/dashboard', authMiddleware, (req, res) => {
res.json({
message: 'Welcome to your dashboard',
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email
},
data: {
lastLogin: new Date().toISOString(),
permissions: ['read', 'write'],
preferences: {
theme: 'dark',
language: 'en'
}
}
});
});
// Token refresh endpoint
app.post('/api/refresh', authMiddleware, (req, res) => {
try {
// Create new token with extended expiration
const newToken = jwt.sign(
{
id: req.user.id,
username: req.user.username,
email: req.user.email
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
message: 'Token refreshed successfully',
token: newToken
});
} catch (error) {
res.status(500).json({ error: 'Failed to refresh token' });
}
});Testing with cURL
Here are some cURL commands to test your authentication system:
# 1. Login to get a token
curl -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{"username": "demo", "password": "password123"}'
# 2. Access protected route (replace TOKEN with actual token)
curl -X GET http://localhost:3000/api/profile \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# 3. Test without token (should fail)
curl -X GET http://localhost:3000/api/profileFrontend Integration Example
Here's how you might integrate this with a frontend application:
// Frontend JavaScript example
class AuthService {
constructor() {
this.baseURL = 'http://localhost:3000/api';
this.token = localStorage.getItem('jwt_token');
}
async login(username, password) {
try {
const response = await fetch(`${this.baseURL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
this.token = data.token;
localStorage.setItem('jwt_token', this.token);
return data;
} else {
throw new Error(data.error);
}
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
async getProfile() {
try {
const response = await fetch(`${this.baseURL}/profile`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
if (response.ok) {
return await response.json();
} else {
throw new Error('Failed to fetch profile');
}
} catch (error) {
console.error('Profile fetch failed:', error);
throw error;
}
}
logout() {
this.token = null;
localStorage.removeItem('jwt_token');
}
}
// Usage
const auth = new AuthService();
// Login
auth.login('demo', 'password123')
.then(result => console.log('Login successful:', result))
.catch(error => console.error('Login failed:', error));
// Get profile
auth.getProfile()
.then(profile => console.log('Profile:', profile))
.catch(error => console.error('Profile fetch failed:', error));π Security Best Practices
When implementing JWT authentication, follow these security best practices:
Token Security
- Use HTTPS: Always transmit tokens over encrypted connections
- Short expiration times: Use short-lived tokens (15-60 minutes)
- Secure storage: Store tokens securely on the client side
- Token rotation: Implement refresh token mechanisms
Secret Management
- Strong secrets: Use cryptographically strong secret keys
- Environment variables: Never hardcode secrets in your code
- Key rotation: Regularly rotate your JWT secrets
- Multiple environments: Use different secrets for dev/staging/production
Input Validation
- Validate all inputs: Always validate and sanitize user inputs
- Rate limiting: Implement rate limiting on authentication endpoints
- Password policies: Enforce strong password requirements
- Account lockout: Implement account lockout after failed attempts
π Next Steps
Now that you have a basic JWT authentication system, consider these enhancements:
- Database integration: Replace the mock user array with a real database
- Refresh tokens: Implement refresh token functionality
- Password reset: Add password reset functionality
- Email verification: Implement email verification for new accounts
- Two-factor authentication: Add 2FA for enhanced security
- Logging and monitoring: Add comprehensive logging and monitoring
Conclusion
You've successfully implemented a complete JWT authentication system in Node.js! This tutorial covered the essential components: token generation, verification middleware, and protected routes. The system provides a solid foundation for building secure, scalable authentication in your applications.
Remember that security is an ongoing process. Regularly review and update your authentication implementation, stay informed about security best practices, and consider using established authentication services for production applications.
For more advanced JWT topics, check out our related articles on JWT fundamentals and building unbreakable JWT security.