Implementing JWT Authentication with Role-Based Access Control in Node.js
Introduction
JSON Web Tokens (JWT) have become the de facto standard for stateless authentication in modern web applications. When combined with Role-Based Access Control (RBAC), JWT provides a powerful and scalable security framework. In this guide, we'll implement a complete JWT authentication system with RBAC in Node.js, covering security best practices and common pitfalls.
Why JWT with RBAC?
JWT offers several advantages for authentication:
- Stateless: No server-side session storage required
- Scalable: Works seamlessly across multiple servers
- Self-contained: Carries user information within the token
- Cross-platform: JSON format works with any technology stack
RBAC adds granular permission control, allowing you to define what users can and cannot do based on their assigned roles.
Setting Up the Foundation
Let's start by setting up our Node.js application with the necessary dependencies:
npm init -y
npm install express jsonwebtoken bcryptjs dotenv helmet cors
npm install --save-dev nodemonCreate a basic Express server structure:
// server.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const helmet = require('helmet');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});User Model and Role System
For this example, we'll use a simple in-memory user store. In production, you'd use a proper database:
// models/User.js
const users = [
{
id: 1,
email: 'admin@example.com',
password: '$2a$10$CwTycUXWue0Thq9StjUM0uJ.nfY5xUgCi5.9EjKS.4Gb5.8Z5V5Yu', // 'password123'
roles: ['admin', 'user']
},
{
id: 2,
email: 'user@example.com',
password: '$2a$10$CwTycUXWue0Thq9StjUM0uJ.nfY5xUgCi5.9EjKS.4Gb5.8Z5V5Yu',
roles: ['user']
}
];
class User {
static findByEmail(email) {
return users.find(user => user.email === email);
}
static findById(id) {
return users.find(user => user.id === id);
}
static hasRole(user, role) {
return user.roles && user.roles.includes(role);
}
static hasAnyRole(user, roles) {
return user.roles && roles.some(role => user.roles.includes(role));
}
}
module.exports = User;JWT Service Implementation
Create a robust JWT service with token generation and validation:
// services/jwtService.js
const jwt = require('jsonwebtoken');
class JWTService {
static generateTokens(payload) {
const accessToken = jwt.sign(
payload,
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m', issuer: 'your-app-name' }
);
const refreshToken = jwt.sign(
{ userId: payload.userId },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d', issuer: 'your-app-name' }
);
return { accessToken, refreshToken };
}
static verifyAccessToken(token) {
try {
return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
} catch (error) {
throw new Error('Invalid access token');
}
}
static verifyRefreshToken(token) {
try {
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
}
module.exports = JWTService;Authentication Middleware
Implement middleware for JWT verification and role-based authorization:
// middleware/auth.js
const JWTService = require('../services/jwtService');
const User = require('../models/User');
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.substring(7);
try {
const decoded = JWTService.verifyAccessToken(token);
const user = User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
const authorize = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!User.hasAnyRole(req.user, roles)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
module.exports = { authenticate, authorize };Authentication Routes
Create endpoints for login, token refresh, and protected routes:
// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const JWTService = require('../services/jwtService');
const User = require('../models/User');
const { authenticate, authorize } = require('../middleware/auth');
const router = express.Router();
// Login endpoint
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = User.findByEmail(email);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = JWTService.generateTokens({
userId: user.id,
email: user.email,
roles: user.roles
});
res.json({
message: 'Login successful',
user: {
id: user.id,
email: user.email,
roles: user.roles
},
...tokens
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Token refresh endpoint
router.post('/refresh', (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
const decoded = JWTService.verifyRefreshToken(refreshToken);
const user = User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
const tokens = JWTService.generateTokens({
userId: user.id,
email: user.email,
roles: user.roles
});
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Protected routes examples
router.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
router.get('/admin', authenticate, authorize(['admin']), (req, res) => {
res.json({ message: 'Admin access granted' });
});
router.get('/users', authenticate, authorize(['admin', 'manager']), (req, res) => {
res.json({ message: 'User management access' });
});
module.exports = router;Environment Configuration
Create a .env file with secure secrets:
JWT_ACCESS_SECRET=your-super-secret-access-key-minimum-32-characters
JWT_REFRESH_SECRET=your-super-secret-refresh-key-minimum-32-characters
PORT=3000Security Best Practices
When implementing JWT authentication, follow these security guidelines:
- Use strong secrets: Generate cryptographically secure random keys
- Short-lived access tokens: Keep access tokens under 15 minutes
- Secure token storage: Store tokens in httpOnly cookies or secure storage
- Implement token blacklisting: For logout and token revocation
- Validate all inputs: Sanitize and validate user inputs
- Rate limiting: Implement login attempt limits
Testing the Implementation
Test the authentication system using tools like Postman or curl:
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "password123"}'
# Access protected route
curl -X GET http://localhost:3000/auth/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Conclusion
This implementation provides a solid foundation for JWT-based authentication with RBAC in Node.js applications. Remember to adapt the code to your specific database system, implement proper error handling, and consider additional security measures like refresh token rotation and proper logging for production environments.
Related Posts
Building Secure Authentication with JWT: Best Practices for 2024
Learn how to implement bulletproof JWT authentication with proper security measures to protect your applications from common attacks.
Building Secure Authentication with JWT and Refresh Tokens: A Complete Guide
Learn to implement robust JWT authentication with refresh tokens, preventing common security vulnerabilities in modern web applications.
Building Secure JWT Authentication with Refresh Tokens in Node.js
Learn how to implement bulletproof JWT authentication with refresh tokens to prevent security vulnerabilities in your Node.js applications.