Empowering Express.js App with Role-Based Access Control: Inspired by AWS IAM Policy to Separate Admin from General Users

Introduction: In the world of cloud computing and secure access management, AWS IAM (Identity and Access Management) policies have been an inspiration for many developers to implement effective role-based access control. This blog explores how AWS IAM Policy motivated the creation of a robust Role-Based Access Control (RBAC) system within an Express.js application. We'll delve into the implementation of authentication and authorization mechanisms using JWT tokens, MongoDB schemas, and middleware for role-based authorization.

Understanding AWS IAM Policy: AWS IAM Policy is a powerful tool that allows users to define permissions for various AWS resources and services. It helps separate administrative privileges from general user access, ensuring security and control over sensitive data and functionalities. This inspired me to apply similar principles within the Express.js app to manage user roles effectively.

Admin Role IAM Policy

//Admin Role IAM Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "user:Delete",
        "task:Issue"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "user:ListAll"
      ],
      "Resource": "arn:aws:users:::*"
    }
  ]
}

General User Role IAM Policy

//User Role IAM Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "user:Register",
        "user:Login",
        "user:ViewOwnProfile",
        "user:Logout"
      ],
      "Resource": "*"
    }
  ]
}

Authentication with JWT: To secure our application, I created an auth.js file where JSON Web Tokens (JWT) were used for authentication. JWTs provided a secure way to generate tokens for authenticated users, ensuring their identity and authenticity.

const jwt = require("jsonwebtoken");
const User = require("../model/user");

const auth = async (req, res, next) => {
  try {
    const token = req.header("Authorization").replace("Bearer ", "");
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findOne({
      _id: decoded._id,
      "tokens.token": token,
    });

    if (!user) {
      throw new Error();
    }

    req.token = token;
    req.user = user;
    next();
  } catch (e) {
    res.status(401).send({ error: "please authenticate." });
  }
};

module.exports = auth;

User Model with MongoDB Schema: I leveraged MongoDB's flexible schema design to create a user model with a designated role field. The role field was implemented as an enum, allowing me to define options for admin and user. By default, users were assigned the user role, while administrators received the admin role

const mongoose = require("mongoose");
const validator = require("validator");
const jwt = require("jsonwebtoken");

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
      lowercase: true,
      validate: {
        validator: (value) => {
          if (!validator.isEmail(value)) {
            throw new Error("Email is invalid");
          }
        },
      },
    },
    password: {
      type: "String",
      trim: true,
      required: true,
      minLength: 7,
      validate: {
        validator: (value) => {
          if (value.toLowerCase().includes("password")) {
            throw new Error("incorrect password");
          }
        },
      },
    },
    roles: {
      type: String,
      required: true,
      enum: ["admin", "read-only"],
      default: "read-only",
    },

    age: {
      type: Number,
      default: 0,
      validate: {
        validator: (value) => {
          if (value < 0) {
            throw new Error("Age must be a positive number");
          }
        },
      },
    },

    tokens: [
      {
        token: {
          type: String,
          required: true,
        },
      }, ],
  },
  {
    timestamps: true,
  }
);

userSchema.methods.generateAuthToken = async function () {
  const user = this;
  const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET);

  user.tokens = user.tokens.concat({ token });
  await user.save();

  return token;
};

const User = mongoose.model("User", userSchema);

module.exports = User;

Middleware for Admin Role Authorization: To enforce role-based authorization, I developed a middleware specifically for the admin role. This middleware validates the user's role before allowing access to sensitive functionalities. This approach ensured that only users with the admin role could perform actions such as deleting users and assigning tasks.

const isAdmin = (req, res, next) => {
  if (req.user && req.user.roles === "admin") {
    next();
  } else {
    res.status(403).send("Access denied.");
  }
};

module.exports = isAdmin;

Functionality for Users and Admins:

User Functionality: General users were restricted to specific actions, including user registration, login, viewing their profile, viewing tasks, completion of tasks and logging out. This limited their access to critical functionalities, maintaining a secure environment.

const express = require("express");
const { auth } = require("../middleware/auth");
const router = new express.Router();
const {
  userCreate,
  userLogin,
  userProfile,
  userLogout,
  viewTask,
  completeTask
} = require("../controllers/userController");

router.post("/users", userCreate);

router.post("/users/login", userLogin);

router.get("/users/me", auth, userProfile);

router.post("/users/logout", auth, userLogout);

router.get("/users/tasks", auth, viewTask);

router.patch("/users/tasks/:id", auth, completeTask);

module.exports = router;

Admin Functionality: Administrators were granted additional privileges, enabling them to perform actions such as deleting users and assigning tasks. Moreover, admins had the authority to view a comprehensive list of all users, offering better control and oversight.

const express = require("express");
const router = new express.Router();
const { usersView, createTask, deleteUser } = require("../controllers/AdminController");
const { auth, isAdmin } = require("../middleware/auth");

//only Admin allowed to access these routes
router.post("/admin/newTask", auth, isAdmin, createTask);
router.post("/admin/deleteUser/:id", auth, isAdmin, deleteUser);
router.get("/admin/users", auth, isAdmin, usersView);

module.exports = router;

Conclusion: Inspired by AWS IAM Policy, I successfully implemented Role-Based Access Control within our Express.js application. By employing JWT tokens for authentication and MongoDB schemas with a role-based field, I could distinguish between administrators and general users. The middleware for admin role authorization ensured that sensitive actions were safeguarded and restricted to authorized personnel. This Express.js app with a robust access control system fosters a secure and efficient environment for both users and administrators, illustrating the potential of IAM policies in influencing access management solutions beyond the scope of AWS.