This project demonstrates how to implement Role-Based Access Control (RBAC) using JWT authentication in an Express.js application. The app will handle authentication and authorization based on user roles: Admin
, Editor
, and Viewer
. Each role has different levels of access to various routes.
Start by creating a new Node.js project with yarn
. If you haven’t initialized your project yet, you can do so by running:
yarn init -y
This will generate a package.json
file with default values.
Install the necessary dependencies for your Express.js application:
yarn add express jsonwebtoken bcryptjs multer dotenv
yarn add nodemon --dev
These dependencies include:
- express – Web framework for Node.js.
- jsonwebtoken – For generating and verifying JWT tokens.
- bcryptjs – For hashing passwords.
- dotenv – To manage environment variables.
- multer – For handling form data (if you need file uploads).
- nodemon – To automatically restart the server in development.
Here's the modify package.json
including the scripts
section you requested:
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
...
}
test
: This is a placeholder for any tests you want to write. You can replace this with your testing framework's script later if you decide to add tests.start
: Runs the server usingnode server.js
for production or production-like environments.dev
: Runs the server withnodemon server.js
, which automatically restarts the server whenever you make changes in the code during development.
After adding this package.json
, make sure to run:
yarn install
This will install all the dependencies listed in the dependencies
section.
Create a .env
file in the root directory of your project to store sensitive information such as the JWT secret key:
JWT_SECRET=your_secret_key_here
PORT=5000
Make sure to replace your_secret_key_here
with a secure key used to sign the JWT tokens.
Here’s an overview of the project’s directory structure:
express-rbac/
│
├── controllers/
│ ├── auth.controller.js
│ ├── resource.controller.js
│
├── routes/
│ ├── auth.route.js
│ ├── resource.route.js
│
├── middlewares/
│ ├── auth.js
│
├── utils/
│ ├── data.js (Contains Users data)
│
├── server.js
├── .env
├── package.json
└── README.md
In utils/data.js
, we store an array of users with hashed passwords. These users will be used for authentication and authorization.
const bcrypt = require('bcryptjs');
const users = [
{
username: 'admin',
password: bcrypt.hashSync('adminpassword', 10),
role: 'Admin'
},
{
username: 'editor',
password: bcrypt.hashSync('editorpassword', 10),
role: 'Editor'
},
{
username: 'viewer',
password: bcrypt.hashSync('viewerpassword', 10),
role: 'Viewer'
}
];
const resources = [
{ id: 1, name: "Resource 1" },
{ id: 2, name: "Resource 2" },
{ id: 3, name: "Resource 3" },
];
module.exports = { users, resources };
The auth.controller.js
file handles the login route, where users can authenticate with their credentials and obtain a JWT token.
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { users } = require('../utils/data'); // Import users from data.js
// Handle login and generate a JWT token
const login = (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Compare the hashed password
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err || !isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
const token = jwt.sign({ username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
};
module.exports = { login };
The resource.controller.js
file handles operations for creating, updating, retrieving, and deleting resources.
const { resources } = require('../utils/data');
// Get all resources
const getResources = (req, res) => {
res.status(200).json({ message: 'Resource available', resources });
};
// Create a new resource
const createResource = (req, res) => {
const newResource = { id: resources.length + 1, name: req.body.name };
resources.push(newResource);
res.status(201).json({ message: 'Resource created', resource: newResource });
};
// Update an existing resource
const updateResource = (req, res) => {
const resource = resources.find(r => r.id === parseInt(req.params.id));
if (!resource) {
return res.status(404).json({ message: 'Resource not found' });
}
resource.name = req.body.name;
res.status(200).json({ message: 'Resource updated', resource });
};
// Delete a resource
const deleteResource = (req, res) => {
const index = resources.findIndex(r => r.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ message: 'Resource not found' });
}
resources.splice(index, 1);
res.status(200).json({ message: 'Resource deleted' });
};
module.exports = { getResources, createResource, updateResource, deleteResource };
This file defines the login route for obtaining the JWT token.
const express = require('express');
const { login } = require('../controllers/auth.controller');
const router = express.Router();
// Login Route
router.post('/login', login);
module.exports = router;
The resource routes allow users to access resources based on their roles.
const express = require('express');
const { verifyAuth, checkRole } = require('../middlewares/auth');
const { getResources, createResource, updateResource, deleteResource } = require('../controllers/resource.controller');
const multer = require('multer');
const router = express.Router();
const upload = multer(); // Middleware for handling form data
// Get all resources
router.get('/', verifyAuth, checkRole(['Admin', 'Editor', 'Viewer']), getResources);
// Create a new resource
router.post('/', verifyAuth, checkRole(['Admin', 'Editor']), upload.none(), createResource);
// Update an existing resource
router.put('/:id', verifyAuth, checkRole(['Admin', 'Editor']), upload.none(), updateResource);
// Delete a resource
router.delete('/:id', verifyAuth, checkRole(['Admin']), deleteResource);
module.exports = router;
In middlewares/auth.js
, the verifyAuth middleware ensures the user is authenticated, and checkRole ensures the user has the correct role to access specific routes.
const jwt = require('jsonwebtoken');
// Middleware to verify JWT token
const verifyAuth = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(403).json({ message: 'No token provided' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid token' });
}
req.user = decoded; // Store user info in request
next();
});
};
// Middleware to check user's role
const checkRole = (roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
}
next();
};
};
module.exports = { verifyAuth, checkRole };
In server.js
, we configure the Express server and set up the routes.
const express = require('express');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
app.use(express.json());
const authRoute = require('./routes/auth.route');
const resourceRoute = require('./routes/resource.route');
app.use('/auth', authRoute);
app.use('/resource', resourceRoute);
app.use('/', (req, res) => {
res.send('Hello Express with RBAC!');
});
const port = process.env.PORT || 5000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
To log in and obtain a JWT token:
- Method:
POST
- URL:
/auth/login
- Body (JSON):
{ "username": "admin", "password": "adminpassword" }
Expected Response:
{
"token": "your_jwt_token_here"
}
Once you have the JWT token, you can access protected routes by including it in the Authorization
header.
For example, to GET all resources:
- Method:
GET
- URL:
/resource/
- Headers:
Authorization: Bearer your_jwt_token_here
Expected Response:
{
"message": "Resource available",
"resources": [
{ "id": 1, "name": "Resource 1" },
{ "id": 2, "name": "Resource 2" }
]
}
To create a new resource:
- Method:
POST
- URL:
/resource/
- Headers:
Authorization: Bearer your_jwt_token_here
- Body (form-data):
name: New Resource
Expected Response:
{
"message": "Resource created",
"resource": { "id": 3, "name": "New Resource" }
}
To update a resource:
- Method:
PUT
- URL:
/resource/1
- Headers:
Authorization: Bearer your_jwt_token_here
- Body (form-data):
name: Updated Resource
Expected Response:
{
"message": "Resource updated",
"resource": { "id": 1, "name": "Updated Resource" }
}
To delete a resource:
- Method:
DELETE
- URL:
/resource/1
- Headers:
Authorization: Bearer your_jwt_token_here
Expected Response:
{
"message": "Resource deleted"
}
- Admin: Full access to all routes (Get, Post, Put, Delete).
- Editor: Can access Get, Post, and Put routes, but not Delete.
- Viewer: Can only access the Get route.
To start the application, run:
yarn dev
This will start the server with nodemon
for automatic restarts during development.
This tutorial has demonstrated how to implement RBAC and JWT Authentication in an Express.js application. You can now protect routes based on user roles and ensure that only authorized users have access to certain resources.