Implementing Role-Based Authentication and Authorization in Node.js

Prashant BalePrashant Bale
7 min read

In modern web applications, managing user authentication and authorization is crucial for ensuring security and proper access control.

Authentication

Simply, we know that authentication is nothing but verifying the user identities for security purposes. Previously (old approach) we used server-based authentication where logged information was stored in the server by creating a session for further identification. Think about the stored logged information which is going to match with the logged user for identity on every request to the server for serving data. This may cause performance issues while handling more authenticated responses by the server.

Token Based Authentication

Here comes token based authentication which means the server will respond with a generated token on user login which will be saved in the client instead of stored in the server to use for further requests. On each client request the token needs to pass with the header which will verify in the server to serve data. The thought is much simpler, once you log in just request with a valid token to get data on each request. Different types of NodeJS token-based authentication:

  • Passport

  • JSON Web Tokens (JWT)

In our application, we are going to use JWT to secure our APIs.

JSON Web Tokens

According to the JWT website: “JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.” Simply JSON Web Token (JWT) is an encoded string to pass information between parties securely.

Following is the JWT string sample:

“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InNoYXNoYW5na2EiLCJwYXNzd29yZCI6IjEyMzQ1IiwiaWF0IjoxNTM2MjgwMjM1LCJleHAiOjE1MzYyODAyNjV9.iplar3jWiW8rh1gU1H6pYaPu6-njCfflrP8GLbx9Imw”

It has three parts separated by “.”, where the first part is header, then the next part is payload, then the signature comes last. While a user requests data to the server this JWT string needs to pass with the header to verify in the server for user identification.

This article explores implementing a role-based access control system using Node.js and Express, inspired by the node-js-authentication-authorization repository.

GitHub Repository: Click here

Features

The project showcases:

  • User Authentication: Registration and login functionalities.

  • Role-Based Authorization: Assigning roles to users and restricting access based on roles.

  • Password Security: Utilizing bcrypt for hashing passwords.

  • Token-Based Authentication: Implementing JSON Web Tokens (JWT) for session management.

  • Protected Routes: Ensuring only authorized users can access specific endpoints.

Tech Stack

The application leverages the following technologies:

  • Node.js: JavaScript runtime environment.

  • Express.js: Web framework for Node.js.

  • MongoDB: NoSQL database for storing user data.

  • bcrypt: Library for hashing passwords securely.

  • jsonwebtoken: Module to handle JWT creation and verification.

  • dotenv: Module to manage environment variables.

Getting Started

To set up the project locally:

  1. Clone the Repository:

     git clone https://github.com/prbale/node-js-authentication-authorization.git
     cd node-js-authentication-authorization
    
  2. Install Dependencies:

     npm install
    
  3. Configure Environment Variables:

    Create a .env file in the root directory with the following content:

     PORT=3000
     MONGODB_URI=your_mongodb_connection_string
     JWT_SECRET=your_jwt_secret_key
    
  4. Start the Application:

     npm start
    

    The server should now be running on http://localhost:3000.

Project Structure

The repository is organized as follows:

  • src/: Contains the main application code.

    • controllers/: Defines functions to handle requests and responses.

    • models/: Contains Mongoose schemas and models for MongoDB.

    • routes/: Defines application routes and associates them with controllers.

    • middlewares/: Includes middleware functions for authentication and authorization.

    • utils/: Utility functions and helpers.

Authentication and Authorization Workflow

  1. User Registration:

    • Users provide a username and password.

    • Passwords are hashed using bcrypt before storing in the database.

  2. User Login:

    • Users authenticate with their credentials.

    • Upon successful authentication, a JWT is issued.

  3. Role Assignment:

    • Users are assigned roles (e.g., admin, user) upon registration or by an admin.
  4. Protected Routes:

    • Middleware checks for the presence of a valid JWT.

    • User roles are verified to authorize access to specific routes.

Important Code Snippets

1. User Model (models/User.js)

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({

    username: {
        type: String,
        required: true,
        unique: true,
    },
    password: {
        type: String,
        required: true,
    },
    role: {
        type: String,
        required: true,
        enum: ["admin", "manager", "user"],
    },

}, {
    timestamps: true,
})

module.exports = mongoose.model('User', userSchema);

Explanation:

  • username: Unique identifier for each user.

  • password: Hashed password stored securely.

  • role: Assigned role for authorization.


2. Authentication Controller (controllers/authController.js)

User Registration

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const register = async (req, res) => {
  const { username, password, role } = req.body;

  try {
    const hashedPassword = await bcrypt.hash(password, 10);
    const newUser = new User({ username, password: hashedPassword, role });
    await newUser.save();
    res.status(201).json({ message: 'User registered successfully' });
  } catch (error) {
    res.status(500).json({ error: 'Error registering user' });
  }
};

Explanation:

  1. Password is hashed with bcrypt.

  2. User details, including the role, are stored in MongoDB.

  3. Error handling ensures robustness.

User Login

const loginUser = async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await User.findOne({ username });
    if (!user) return res.status(404).json({ error: 'User not found' });

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) return res.status(401).json({ error: 'Invalid credentials' });

    const token = jwt.sign({ userId: user._id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' });

    res.status(200).json({ token });
  } catch (error) {
    res.status(500).json({ error: 'Error logging in' });
  }
};

Explanation:

  1. User Lookup: Check if the user exists.

  2. Password Validation: Compare the provided password with the hashed one.

  3. Token Generation: JWT includes the user's ID and role.


3. Authorization Middleware (middlewares/authMiddleware.js)

Token Verification

const jwt = require('jsonwebtoken');

const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access denied' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: 'Invalid token' });
  }
};

Explanation:

  • Extract the token from the Authorization header.

  • Verify the token using the secret key.

  • Attach the decoded user information to req.user.

Role-Based Access Control

const authorizeRole = (roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Access Denied' });
    }
    next();
  };
};

Explanation:

  • roles is an array of permitted roles.

  • Checks if the user’s role matches any in the provided array.


4. Protected Route (routes/protectedRoute.js)

const express = require('express');
const { authenticateUser, authorizeRole } = require('../middlewares/authMiddleware');

const router = express.Router();

router.get('/admin', authenticateUser, authorizeRole(['admin']), (req, res) => {
  res.status(200).json({ message: 'Welcome to the admin dashboard!' });
});

module.exports = router;

Explanation:

  • The /admin endpoint is accessible only to authenticated users with the admin role.

5. App Entry Point (app.js or index.js)

const express = require('express');
const mongoose = require('mongoose');
const authRoutes = require('./routes/authRoutes');
const protectedRoutes = require('./routes/protectedRoutes');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/protected', protectedRoutes);

// Database Connection
mongoose.connect(process.env.MONGODB_URI)
  .then(() => console.log('MongoDB connected'))
  .catch((err) => console.error('MongoDB connection failed:', err));

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Explanation:

  • Express Setup: Sets up middleware and routes.

  • Database Connection: Connects to MongoDB using Mongoose.

  • Environment Variables: Loads environment-specific settings.


6. Testing the Application

Register a User

curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
  "username": "adminUser",
  "password": "password123",
  "role": "admin"
}'

Login the User

curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
  "username": "adminUser",
  "password": "password123"
}'

This should return a JWT token.

Access Protected Route

curl -X GET http://localhost:3000/api/protected/admin \
-H "Authorization: Bearer <your_token>"

Expected Response (if authorized):

{
  "message": "Welcome admin"
}

7. Security Best Practices

  1. Use Strong Secrets: Never hardcode secret keys; store them in environment variables.

  2. Token Expiry: Use short-lived tokens and implement token refreshing strategies.

  3. Password Hashing: Use strong hashing algorithms like bcrypt with sufficient rounds.

  4. Input Validation: Sanitize and validate all user inputs to avoid injection attacks.

  5. HTTPS: Always use HTTPS in production for secure communication.


8. Conclusion

The node-js-authentication-authorization repository demonstrates a practical implementation of authentication and role-based authorization using Node.js. By leveraging JWT, bcrypt, and Express.js, the application ensures secure user authentication while maintaining clean and modular code.

Explore the code and enhance it by adding features like account lockout mechanisms, password reset functionalities, and more advanced role hierarchies.

Happy coding! 🎯

0
Subscribe to my newsletter

Read articles from Prashant Bale directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Prashant Bale
Prashant Bale

With 17+ years in software development and 14+ years specializing in Android app architecture and development, I am a seasoned Lead Android Developer. My comprehensive knowledge spans all phases of mobile application development, particularly within the banking domain. I excel at transforming business needs into secure, user-friendly solutions known for their scalability and durability. As a proven leader and Mobile Architect.