Creating production-ready authentication and authorization requires robust security practices. Below is a complete, modular setup for an Express application using JWT, HttpOnly cookies, and role-based access control.
Prerequisites
Project Setup:
bash
mkdir express-auth-api
cd express-auth-api
npm init -y
npm install express jsonwebtoken cookie-parser dotenv
Use code with caution.
Environment File: Create a .env file in your project root:
env
.env
JWT_SECRET=YOUR_VERY_STRONG_RANDOM_SECRET_KEY_HERE
NODE_ENV=development
Use code with caution.
The Production-Ready Code
This solution is broken down into four files:
server.js: The main application file.
middleware/auth.js: The authentication middleware.
middleware/roleAuth.js: The authorization middleware.
routes/authRoutes.js: Separate routes for login/logout (good practice for larger apps).
- server.js (Main Application File) This file sets up the server, connects middleware, and defines protected routes. javascript // server.js const express = require('express'); const cookieParser = require('cookie-parser'); const dotenv = require('dotenv'); const authRoutes = require('./routes/authRoutes'); const authenticateToken = require('./middleware/auth'); const authorizeRole = require('./middleware/roleAuth');
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
const isProduction = process.env.NODE_ENV === 'production';
// --- Middleware Setup ---
app.use(express.json()); // Allows parsing JSON bodies from POST requests
app.use(cookieParser()); // Allows parsing cookies from incoming requests
// --- Routes ---
// Use separate route files for authentication (login/logout)
app.use('/api/auth', authRoutes);
// Example Public Route
app.get('/api/public', (req, res) => {
res.status(200).json({ message: 'This is a public endpoint.' });
});
// --- Protected Routes (Examples) ---
// Route accessible by all authenticated users
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user contains { id, username, role } thanks to authenticateToken
res.status(200).json({
message: Welcome ${req.user.username}, you are authenticated!,
user: req.user
});
});
// Route accessible only by 'admin' users
app.get('/api/admin-dashboard', authenticateToken, authorizeRole('admin'), (req, res) => {
res.status(200).json({
message: 'Welcome to the Admin Dashboard, only visible to Admins!'
});
});
// Route accessible only by 'user' role
app.get('/api/user-dashboard', authenticateToken, authorizeRole('user'), (req, res) => {
res.status(200).json({
message: 'Welcome to the User Dashboard.'
});
});
// --- Server Start ---
app.listen(PORT, () => {
console.log(Server running on http://localhost:${PORT});
});
Use code with caution.
- middleware/auth.js (Authentication Middleware) This middleware verifies the JWT and attaches the decoded user data to the request object. javascript // middleware/auth.js const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
// 1. Extract the token from the HttpOnly cookie
const token = req.cookies && req.cookies.accessToken;
if (token == null) {
// If no token exists in the cookie, the user is not authenticated.
return res.status(401).json({ message: 'Authentication required. No token provided.' });
}
// 2. Verify the token's signature using the secret key
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
// If the token is invalid (expired, manipulated, etc.)
return res.status(403).json({ message: 'Forbidden. Invalid or expired token.' });
}
// 3. If valid, attach the decoded user payload ({ id, username, role }) to the request
req.user = user;
// 4. Pass control to the next middleware or route handler
next();
});
}
module.exports = authenticateToken;
Use code with caution.
- middleware/roleAuth.js (Authorization Middleware) This middleware uses the req.user data provided by authenticateToken to check for specific roles. javascript // middleware/roleAuth.js
/**
- Factory function to create authorization middleware for a specific required role.
-
@param {string} requiredRole - The role needed for access (e.g., 'admin', 'user')
*/
function authorizeRole(requiredRole) {
return (req, res, next) => {
// We assume req.user has been populated by the authenticateToken middleware
if (!req.user || req.user.role !== requiredRole) {
return res.status(403).json({
message:Access denied. You do not have the required role: ${requiredRole}.
});
}// Role matches, proceed to the next handler next();};
}
module.exports = authorizeRole;
Use code with caution.
- routes/authRoutes.js (Login/Logout Logic) Separating auth logic keeps your main server file clean. javascript // routes/authRoutes.js const express = require('express'); const jwt = require('jsonwebtoken'); const router = express.Router();
// Mock database of users (replace with a real DB query in production)
const mockUsersDb = [
{ id: 1, username: 'user', role: 'user', passwordHash: 'password123' },
{ id: 2, username: 'admin', role: 'admin', passwordHash: 'adminpass' }
];
const isProduction = process.env.NODE_ENV === 'production';
// --- Login Route ---
router.post('/login', (req, res) => {
const { username, password } = req.body;
const user = mockUsersDb.find(u => u.username === username);
// In a production app, use bcrypt to compare password hashes
if (!user || user.passwordHash !== password) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Payload includes necessary claims, crucially the 'role'
const payload = { id: user.id, username: user.username, role: user.role };
const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
// Set the HttpOnly cookie for secure storage
res.cookie('accessToken', accessToken, {
httpOnly: true, // Prevents client-side JS access (XSS mitigation)
secure: isProduction, // Ensures cookie is sent only over HTTPS in production
sameSite: 'Strict', // Mitigates CSRF
maxAge: 3600000 // 1 hour expiration
});
res.status(200).json({ message: 'Login successful', role: user.role });
});
// --- Logout Route ---
router.post('/logout', (req, res) => {
res.clearCookie('accessToken', {
httpOnly: true,
secure: isProduction,
sameSite: 'Strict',
});
res.status(200).json({ message: 'Logged out successfully' });
});
module.exports = router;
Use code with caution.
Top comments (0)