Skip to content

Instantly share code, notes, and snippets.

@programmerShinobi
Created November 9, 2024 10:10
Show Gist options
  • Save programmerShinobi/161d7a8c220bb1059d613a51aeb0cc6b to your computer and use it in GitHub Desktop.
Save programmerShinobi/161d7a8c220bb1059d613a51aeb0cc6b to your computer and use it in GitHub Desktop.

RBAC with JWT Authentication in Express

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.


Project Setup

1. Initialize the Project

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.

2. Install Dependencies

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:

package.json:

{
 ... 
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  ...
}

Explanation of scripts section:

  • 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 using node server.js for production or production-like environments.
  • dev: Runs the server with nodemon 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.


3. Environment Configuration

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.


File Structure

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

Data Users

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 };

Controllers

1. Auth Controller (auth.controller.js)

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 };

2. Resource Controller (resource.controller.js)

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 };

Routes

1. Authentication Routes (auth.route.js)

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;

2. Resource Routes (resource.route.js)

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;

Middlewares

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 };

Server Setup

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}`);
});

Testing with Postman

1. Login and Get JWT Token

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"
}

2. Accessing Protected Routes

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"
}

Roles and Permissions

  • 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.

Running the Application

To start the application, run:

yarn dev

This will start the server with nodemon for automatic restarts during development.


Conclusion

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.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment