How to implement role-based access control in Express.js REST API using Passport.js and JWT

Role-based access control is a secure authentication mechanism. You can use it to restrict access to certain resources.

This type of authentication helps system administrators control access rights according to user roles. This granular level of control adds an extra layer of security, allowing apps to block unauthorized access.

Implement role-based access using Passport.js and JWT

You have two ways to do this including using a dedicated library like AcessControl or leveraging an existing validation library to implement this mechanism.

How to implement role-based access control in Express.js REST API using Passport.js and JWT Picture 1How to implement role-based access control in Express.js REST API using Passport.js and JWT Picture 1

 

Here, JSON Web Token (JWT) provides a secure way to transfer authentication information, and Passport.js simplifies the authentication process by providing flexible authentication middleware.

Using this approach, you can allocate user roles, encrypting them in the JWT when they authenticate. You can then use JWT to verify the user's identity & role in subsequent queries, enabling access control and role decentralization.

Both methods have their own advantages. They work effectively for role-based access control. Which method to choose depends on your project requirements.

Express.js . project setup

To get started, set up an Express.js project locally. Then install these packages:

npm install cors dotenv mongoose cookie-parser jsonwebtoken mongodb passport passport-local

Next, create a MongoDB database or set up a cluster on MongoDB Atlas. Copy the database connection URL and add it to the .env file in the project root directory:

CONNECTION_URI="connection URI"

Configure database connection

In the root directory, create a new file utils/db.js , add the code below to establish a connection to the MongoDB bucket running on Atlas using Mongoose.

const mongoose = require('mongoose'); const connectDB = async () => { try { await mongoose.connect(process.env.CONNECTION_URI); console.log("Connected to MongoDB!"); } catch (error) { console.error("Error connecting to MongoDB:", error); } }; module.exports = connectDB;

Define data model

In the root directory, create a new file model/user.model.js and add the following code to define the data model for the user using Mongoose.

const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ username: String, password: String, role: String }); module.exports = mongoose.model('User', userSchema);

Create controller for API Endpoints

Create a new controllers/user.controller.js file in the root directory and add the code below.

First, create these imports:

const User = require('./models/user.model'); const passport = require('passport'); const { generateToken } = require('./middleware/auth'); require('./middleware/passport')(passport);

 

Next, define the user registration management logic and login functionality:

exports.registerUser = async (req, res) => { const { username, password, role } = req.body; try { await User.create({ username, password, role }); res.status(201).json({ message: 'User registered successfully' }); } catch (error) { console.log(error); res.status(500).json({ message: 'An error occurred!' }); } }; exports.loginUser = (req, res, next) => { passport.authenticate('local', { session: false }, (err, user, info) => { if (err) { console.log(err); return res.status(500).json({ message: 'An error occurred while logging in' }); } if (!user) { return res.status(401).json({ message: 'Invalid login credentials' }); } req.login(user, { session: false }, (err) => { if (err) { console.log(err); return res.status(500).json({ message: 'An error occurred while logging in' }); } const { _id, username, role } = user; const payload = { userId: _id, username, role }; const token = generateToken(payload); res.cookie('token', token, { httpOnly: true }); return res.status(200).json({ message: 'Login successful' }); }); })(req, res, next); };

The registerUser function handles new user registration by retrieving the username, password, and role from the query body. It then creates a new user entry in the database and responds with a success or error message if any problems occur during the process.

The loginUser function, on the other hand, facilitates the user by using the local authentication tactic provided by Passport.js. It validates the user's information and returns the token after successful login. That token is stored in a cookie for subsequent authentication requests. If an error occurs during login, it will return an appropriate message.

Finally, add code that implements the logic of fetching all user data from the database. The article uses this endpoint as a restricted route to ensure that only authenticated users with admin rights can access this endpoint.

exports.getUsers = async (req, res) => { try { const users = await User.find({}); res.json(users); } catch (error) { console.log(error); res.status(500).json({ message: 'An error occurred!' }); } };

Setting up Passport.js . Local Authentication Tactics

To authenticate users after they provide credentials, you need to set up a local authentication tactic.

 

Create a new middleware/passport.js file in the root directory and add the following code.

const LocalStrategy = require('passport-local').Strategy; const User = require('./models/user.model'); module.exports = (passport) => { passport.use( new LocalStrategy(async (username, password, done) => { try { const user = await User.findOne({ username }); if (!user) { return done(null, false); } if (user.password !== password) { return done(null, false); } return done(null, user); } catch (error) { return done(error); } }) ); };

This code defines a local passport.js tactic to authenticate the user based on the provided username and password.

It first asks the database to find a user with a matching username and then continues with password validation. As a result, it returns the authenticated user object if the login was successful.

Create JWT authentication middleware

Inside the middleware folder , create a new auth.js file and add the following code to define the JWT creation & verification middleware.

const jwt = require('jsonwebtoken'); const secretKey = process.env.SECRET_KEY; const generateToken = (payload) => { const token = jwt.sign(payload, secretKey, { expiresIn: '1h' }); return token; }; const verifyToken = (requiredRole) => (req, res, next) => { const token = req.cookies.token; if (!token) { return res.status(401).json({ message: 'No token provided' }); } jwt.verify(token, secretKey, (err, decoded) => { if (err) { return res.status(401).json({ message: 'Invalid token' }); } req.userId = decoded.userId; if (decoded.role !== requiredRole) { return res.status(403).json({ message: 'You do not have the authorization and permissions to access this resource.' }); } next(); }); }; module.exports = { generateToken, verifyToken };

The generateToken function generates a JWT with the specified expiration time, and the verifyToken function checks if the token exists and is valid. In addition, it also validates that the decrypted token contains the required role, essentially ensuring that only authorized and authorized users can access it.

To sign the JWT in a unique way, you need to generate the private secret key and add it to the .env file as shown below.

SECRET_KEY="This is a sample secret key."

Define API routing

In the root directory, create a new directory and name it routes. In this directory, create userRoutes.js and add the following code:

const express = require('express'); const router = express.Router(); const userControllers = require('./controllers/userController'); const { verifyToken } = require('./middleware/auth'); router.post('/api/register', userControllers.registerUser); router.post('/api/login', userControllers.loginUser); router.get('/api/users', verifyToken('admin'), userControllers.getUsers); module.exports = router;

This code defines the HTTP routes for the REST API. Specifically, the users route acts as the protected route. By restricting access to users with the admin role , you effectively enforce role-based access.

Update the main server file

Open the server.js file and update it as follows:

const express = require('express'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const app = express(); const port = 5000; require('dotenv').config(); const connectDB = require('./utils/db'); const passport = require('passport'); require('./middleware/passport')(passport); connectDB(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); app.use(cookieParser()); app.use(passport.initialize()); const userRoutes = require('./routes/userRoutes'); app.use('/', userRoutes); app.listen(port, () => { console.log(`Server is running on port ${port}`); });

 

Finally, start the programming server to run this application.

node server.js

Implementing role-based access control is an effective way to improve application security. Hope this article helps you make this task easier.

5 ★ | 1 Vote