September 22, 202510 min read

RBAC Authentication System: A Complete Guide for Node.js

Dhiraj Kumar

Dhiraj Kumar

Software Engineer & Full-Stack Developer

Node.jsSecurityJWTMongoDBBackend

Every serious application needs authentication and authorization. When I built the Yuga Yatra retail platform, I needed a system where Super Admins could manage everything, Store Managers could handle inventory, and Sales Staff could only process orders. RBAC was the answer.

The Data Model

RBAC starts with a clean data model. I use three collections: Users (credentials, profile), Roles (name, permissions array), and a many-to-many relationship between them:

// Role schema
const RoleSchema = new mongoose.Schema({
  name: { type: String, enum: ['super_admin', 'admin', 'manager', 'sales', 'viewer'] },
  permissions: [{
    resource: { type: String, enum: ['products', 'orders', 'users', 'reports', 'settings'] },
    actions: [{ type: String, enum: ['create', 'read', 'update', 'delete'] }]
  }]
});

// User schema — roles as references
const UserSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  passwordHash: String,
  roles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Role' }],
  refreshTokens: [String]
});

JWT + Refresh Token Flow

Access tokens (short-lived, 15 minutes) carry the user ID and permissions. Refresh tokens (long-lived, 7 days) are stored in an HTTP-only cookie and in the database. When the access token expires, the client silently exchanges the refresh token for a new pair.

// Auth middleware
export async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.sub).populate('roles');
    if (!user) return res.status(401).json({ error: 'User not found' });
    req.user = user;
    req.permissions = user.roles.flatMap(r => r.permissions);
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Permission Middleware

The authorization middleware checks if the user has the required permission for a specific resource and action. This is declarative — each route specifies what it needs:

// Permission middleware factory
export function authorize(resource, action) {
  return (req, res, next) => {
    const hasPermission = req.permissions.some(
      p => p.resource === resource && p.actions.includes(action)
    );
    if (!hasPermission) return res.status(403).json({ error: 'Forbidden' });
    next();
  };
}

// Usage in routes
router.delete('/products/:id',
  authenticate,
  authorize('products', 'delete'),
  productController.deleteProduct
);

Security Best Practices

  • Hash passwords with bcrypt (12 salt rounds minimum)
  • Never store JWT in localStorage — use HTTP-only cookies
  • Implement rate limiting on auth endpoints to prevent brute force
  • Rotate refresh tokens on each use (detect token theft)
  • Audit log all admin actions for compliance
Dhiraj Kumar

Dhiraj Kumar

Dhiraj Kumar is a software engineer and full-stack developer interested in modern web technologies, artificial intelligence, automation, and agentic systems. Through his portfolio and technical writing, he explores practical approaches to building reliable software and AI-powered applications.

Enjoyed this article? Let's connect!

Read More Articles