DocsHub
Security

Authorization

Learn how to control what authenticated users can do — MongoDB built-in roles and application-level role-based access control (RBAC).

Authorization

Authentication answers "who are you?" Authorization answers a different question — what are you allowed to do?

A logged-in teacher and a logged-in student are both authenticated — MongoDB and your app both know who they are. But a teacher should be able to update grades for their own courses, while a student should only be able to view their own grades. That distinction is authorization.

Just like authentication, authorization also has two layers — MongoDB's built-in roles, and your application's own role-based access control.


MongoDB Built-in Roles

MongoDB has built-in roles that control what a database user can do at the database level. We touched on this briefly in the Authentication file when we created schoolAppUser with the readWrite role.

Common Built-in Roles

RoleWhat it allows
readRead data only — no writes
readWriteRead and write data
dbAdminManage indexes, run validation, view stats — no data access changes
userAdminCreate and manage database users
readWriteAnyDatabaseRead and write across all databases
rootFull access to everything — superuser

Example — Different Users for Different Purposes

use school

// Application user — full read/write for the app
db.createUser({
  user: "schoolAppUser",
  pwd: "appPassword",
  roles: [{ role: "readWrite", db: "school" }]
})

// Reporting user — read-only, for analytics dashboards
db.createUser({
  user: "reportingUser",
  pwd: "reportPassword",
  roles: [{ role: "read", db: "school" }]
})

// Backup user — read-only across the cluster
db.createUser({
  user: "backupUser",
  pwd: "backupPassword",
  roles: [{ role: "readAnyDatabase", db: "admin" }]
})

MongoDB roles control access at the database connection level — they apply to whoever holds those credentials. They do not know anything about your app's individual users (teachers, students). For that, you need application-level authorization — covered next.


Application-Level Authorization — Role-Based Access Control (RBAC)

In our school system, every user has a role field — admin, teacher, or student. RBAC means checking this role before allowing an action.

admin teacher student Logged-in User What is their role? Full Access✅ Manage all students✅ Manage all teachers✅ Manage all courses✅ View all reports Limited Access✅ View their own courses✅ Update grades for their courses❌ Cannot edit other teachers' courses❌ Cannot manage users Read-Only, Own Data✅ View their own grades✅ View their own profile❌ Cannot view other students' data❌ Cannot modify anything

Defining Roles in the Schema

We already defined this in the Authentication file:

const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  password: String,
  role: {
    type: String,
    enum: ["admin", "teacher", "student"],
    required: true
  }
});

For teachers and students, we also need to link the User document to their corresponding Teacher or Student document:

const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  password: String,
  role: {
    type: String,
    enum: ["admin", "teacher", "student"],
    required: true
  },
  // Link to the Teacher or Student document, depending on role
  profileId: {
    type: mongoose.Schema.Types.ObjectId,
    refPath: 'role' // dynamically references "Teacher" or "Student" based on role
  }
});

refPath is a Mongoose feature for dynamic references — instead of always pointing to the same model, the ref is determined by the value of another field (role). However, refPath requires the field value to exactly match a model name. Since our roles are lowercase ("teacher") but model names are capitalized ("Teacher"), in practice you would store a separate field like profileModel: "Teacher" for this to work cleanly. For simplicity, many apps just store teacherId and studentId as separate optional fields instead.


Authorization Middleware

Building on the requireAuth middleware from the Authentication file, we add a requireRole middleware that checks the user's role:

function requireRole(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ message: 'Not authenticated' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ message: 'Access denied — insufficient permissions' });
    }

    next();
  };
}

requireRole is a function that returns a middleware function — this lets you pass different allowed roles for different routes.

Using requireRole

const express = require('express');
const app = express();

// Only admins can create teachers
app.post('/api/teachers',
  requireAuth,
  requireRole('admin'),
  async (req, res) => {
    const teacher = await Teacher.create(req.body);
    res.status(201).json(teacher);
  }
);

// Admins and teachers can view course lists
app.get('/api/courses',
  requireAuth,
  requireRole('admin', 'teacher'),
  async (req, res) => {
    const courses = await Course.find();
    res.json(courses);
  }
);

// Any authenticated user can view their own profile
app.get('/api/profile',
  requireAuth,
  async (req, res) => {
    const user = await User.findById(req.user.userId).select('-password');
    res.json(user);
  }
);

401 Unauthorized means "we do not know who you are" — not logged in. 403 Forbidden means "we know who you are, but you cannot do this" — logged in but not allowed.


Resource-Level Authorization — "Own Data Only"

Role checks alone are not enough. A teacher should only update grades for their own courses — not every course in the school. This requires checking ownership of the specific resource being accessed.

Example — Teacher Can Only Update Their Own Course Grades

app.put('/api/courses/:courseId/grades',
  requireAuth,
  requireRole('admin', 'teacher'),
  async (req, res) => {
    const course = await Course.findById(req.params.courseId);

    if (!course) {
      return res.status(404).json({ message: 'Course not found' });
    }

    // Admins can update any course
    if (req.user.role === 'admin') {
      // proceed
    }
    // Teachers can only update their own course
    else if (req.user.role === 'teacher') {
      const teacher = await Teacher.findOne({ userId: req.user.userId });

      if (!teacher || !course.teacherId.equals(teacher._id)) {
        return res.status(403).json({ message: 'You can only update your own courses' });
      }
    }

    // Proceed with grade update
    const { studentId, subject, score } = req.body;

    await Student.updateOne(
      { _id: studentId },
      { $push: { grades: { subject, score, semester: 1 } } }
    );

    res.json({ message: 'Grade updated' });
  }
);

Example — Student Can Only View Their Own Data

app.get('/api/students/:studentId',
  requireAuth,
  async (req, res) => {
    const { studentId } = req.params;

    // Admins and teachers can view any student
    if (req.user.role === 'admin' || req.user.role === 'teacher') {
      const student = await Student.findById(studentId);
      return res.json(student);
    }

    // Students can only view their own record
    if (req.user.role === 'student') {
      const student = await Student.findOne({ userId: req.user.userId });

      if (!student || !student._id.equals(studentId)) {
        return res.status(403).json({ message: 'You can only view your own profile' });
      }

      return res.json(student);
    }

    res.status(403).json({ message: 'Access denied' });
  }
);

This is one of the most commonly missed security checks in real applications — known as an Insecure Direct Object Reference (IDOR). A developer adds requireAuth (is the user logged in?) but forgets to check ownership (is this the user's own data?). Without the ownership check, any logged-in student could view any other student's record just by changing the studentId in the URL.


Building a Permissions Table

For a clearer overview, define what each role can do as a permissions table — and reference it consistently across your routes:

const PERMISSIONS = {
  admin: {
    students: ['create', 'read', 'update', 'delete'],
    teachers: ['create', 'read', 'update', 'delete'],
    courses:  ['create', 'read', 'update', 'delete'],
    grades:   ['read', 'update']
  },
  teacher: {
    students: ['read'],            // can view students
    teachers: ['read'],            // can view other teachers (limited)
    courses:  ['read', 'update'],  // can update their own courses only
    grades:   ['read', 'update']   // can update grades for their courses only
  },
  student: {
    students: ['read'],            // own profile only
    teachers: [],
    courses:  ['read'],            // their enrolled courses only
    grades:   ['read']             // own grades only
  }
};

function hasPermission(role, resource, action) {
  return PERMISSIONS[role]?.[resource]?.includes(action) || false;
}
// Usage in middleware
function requirePermission(resource, action) {
  return (req, res, next) => {
    if (!hasPermission(req.user.role, resource, action)) {
      return res.status(403).json({ message: 'Access denied' });
    }
    next();
  };
}

app.delete('/api/students/:id',
  requireAuth,
  requirePermission('students', 'delete'),
  async (req, res) => {
    await Student.findByIdAndDelete(req.params.id);
    res.json({ message: 'Student deleted' });
  }
);

This permissions table approach centralizes your authorization rules — instead of scattered role checks across routes, you have one place that defines what every role can do.


Our School System — Authorization Summary

ActionAdminTeacherStudent
View all students
View own profile
Create/edit students
Create/edit teachers
View all courses✅ (enrolled only)
Edit own course✅ (own only)
Edit any course
Update grades✅ (own courses)
View own grades
View other students' grades
Delete any record

Quick Reference

// Authentication — who are you?
app.use(requireAuth);

// Authorization — what role do you have?
app.use(requireRole('admin', 'teacher'));

// Resource-level — is this YOUR data?
if (!resource.ownerId.equals(req.user.userId)) {
  return res.status(403).json({ message: 'Access denied' });
}

// Permission table — centralized rules
if (!hasPermission(req.user.role, 'students', 'delete')) {
  return res.status(403).json({ message: 'Access denied' });
}

Always think about authorization in three layers — role (what type of user is this?), resource (does this specific record belong to them?), and action (are they allowed to do this specific thing?). Missing any one of these three checks is how real security vulnerabilities happen — most commonly the resource-level "own data" check, which is easy to forget when a route works correctly for admins during testing.

On this page