⇦ Back

WDX-180

Web Development X

Authorization, Roles & Permissions

Deciding What Users Are Allowed To Do

Authentication answers:

“Who are you?”

Authorization answers:

“What can you do?”

Yesterday we built:

  Login
  Logout
  Sessions

Now every user can log in.

That’s progress.

Unfortunately:

  Every logged-in user
  can still do everything.

That’s less ideal.

Imagine:

  Intern

accidentally deleting:

  Entire Product Catalog

because they found the delete button. 🙀

Today we’ll build a permission system that prevents that.

Learning Objectives

By the end of this lesson, students will be able to:

Part 1 — Authentication vs Authorization

Yesterday:

  Are you logged in?

Today:

  What are you allowed to do?

Example:

User Login Delete Products
Admin Yes Yes
Editor Yes No
Viewer Yes No

Authentication alone is insufficient.

Authorization completes the picture.

Part 2 — The Simplest Role System

Add:

  ALTER TABLE users

  ADD COLUMN role TEXT
  DEFAULT 'viewer';

Update your db/setup.js script and re-initialize the Database in order to test the new authorization rules we are about to implement.

Possible values:

  admin
  editor
  viewer

Example:

  admin@example.com

  admin
  editor@example.com

  editor
  viewer@example.com

  viewer

Update db/setup.js and create 2 new users with an admin and editor role, just to experiment with different permissions.

Part 3 — Loading User Information

Currently:

  req.session.userId

stores only ID.

Load user via a Middleware:

  app.use(function provideUserInfo( req, res, next ){
    if ( req.session.userId) {
        req.user = userRepository.findById(req.session.userId);
    }
    next();
  });

Now:

  req.user

contains:

  {
      id: 1,
      email: 'admin@example.com',
      role: 'admin'
  }

Very useful.

This information will be available throughout our Backend so that we can check if the user is logged in and whether they have the appropriate permissions based on their role.

Part 4 — Creating Authorization Middleware

Authentication middleware:

  requireAuth()

Authorization middleware:

  requireRole()

Example:

  function requireRole(role) {
      return ( req, res, next ) => {
          if (!req.user) {
              return res.redirect('/login');
          }

          if (req.user.role !== role) {
              return res
                  .status(403)
                  .render('403', { title: "403" });
          }
          next();
      };
  }

  module.exports = { requireRole }

You can store the requireRole middleware somewhere in an auth folder, export it and import it where needed: auth/index.js.

Make sure to create the appropriate view views/403.ejs:

  <h1>403</h1>
  <p>Forbidden</p>

Usage:

  const { requireRole } = require("./auth/index.js");

  app.get('/admin', requireRole('admin'), (req,res) => {
        ...
  });

Only admins allowed.

Here’s how the Authentication and Authorization architecture with Express middleware works in our application:

  flowchart LR
      A[Client Request] --> B[Express Router]
      B --> C[provideUserInfo middleware]
      C --> D["requireAuth() middleware"]
      D --> E["requireRole('admin') middleware"]
      E --> F[Route Handler]
      C -->|no user| G[redirect /login]
      D -->|not authenticated| G
      E -->|role mismatch| H[403 Forbidden]
      E -->|role ok| F

Part 5 — Understanding HTTP 403

Authentication failure:

  401 Unauthorized

or redirect.

Authorization failure:

  403 Forbidden

Meaning:

  We know who you are.

  You are not allowed here.

Important distinction.

Perhaps, it’s a good time now to create a 401.ejs file as well inside the views folder.

Part 6 — Protecting CRUD Operations

Question:

Should viewers create products?

Probably not.

Example:

Action Admin Editor Viewer
View Products
Create Product
Edit Product
Delete Product

Routes:

  router.post('/delete/:id', requireRole('admin'), ...);

Editors cannot delete products.

Admins can.

Part 7 — Role Hierarchies

Current:

  role === 'admin'

works.

But:

  admin

should automatically inherit:

  editor
  viewer

permissions.

Hierarchy:

  admin

    ↓

  editor

    ↓

  viewer

Represent numerically:

  const roles = {
      viewer: 1,
      editor: 2,
      admin: 3
  };

Middleware:

  if (roles[req.user.role] < roles[requiredRole]) {
      return res
          .status(403)
          .render('403', { title: "Forbidden" });
  }

Much more flexible.

Part 8 — Hiding UI Elements

Bad:

  Delete Product

visible to everyone.

Even if route is protected.

Better:

  <% if ( user.role === 'admin') { %>
    <a>Delete</a>
  <% } %>

Users only see actions they can perform.

Important:

  UI hiding ≠ Security

Routes must still be protected.

Always.

You can use the res.locals object to provide data access to your views:

  // Through a Middleware:
  app.use(( req, res, next )=>{
    res.locals.userRole = "admin";
  })

  // Through a Route
  app.get("/", (req, res )=>{
    res.locals.customData = { value: 42 };
  });

And, now you can access them from the .ejs files:

  <% if ( customData ) { %>
    Custom data: <%= customData.value %>
  <% } %>

You can now implement a Middleware that lets the views access the userRole and conditionally display the Delete and Edit buttons throughout the Product screens.

Part 9 — Ownership-Based Permissions

Roles aren’t always enough.

Example:

  User A

creates:

  Product A

Question:

Can:

  User B

edit it?

Maybe not.

Add to the Database Schema:

  created_by INTEGER

to products.

Create:

  created_by = req.user.id

Check ownership:

  if ( product.created_by !== req.user.id ) {
      return res
          .status(403)
          .render('403', { title: "Forbidden" });
  }

Very common pattern.

Part 10 — Combining Roles and Ownership

Example:

  Admins

can edit everything.

  Editors

can edit their own.

Logic:

  const canEdit = req.user.role === 'admin' || product.created_by === req.user.id;

Real-world applications often combine both approaches.

Part 11 — Avoiding Privilege Escalation

Dangerous:

  <select name="role">
    <option>admin</option>
  </select>

User submits:

  role=admin

Congratulations.

They promoted themselves.

Never trust:

  Client Input

for permissions.

Server decides permissions.

Always.

Part 12 — Centralizing Permissions

❌ Bad:

  if ( admin ) 

everywhere.

Eventually:

  100 routes

  50 permission checks

becomes chaos.

Create:

  permissions.js

Example:

  module.exports = {
      canDeleteProduct(user) {
          return ( user.role === 'admin');
      }
  };

Cleaner.

Reusable.

Testable.

Part 13 — Building an Admin Dashboard

Protected route:

  app.get('/admin', requireRole('admin'),(req,res) => {
    res.render('admin', { title: "Admin Dashboard" });
  });

Only administrators see:

  System Settings

  User Management

  Audit Logs

A common application architecture.

Part 14 — Future Permission Models

Simple roles work well.

Eventually systems grow.

Example:

  Create Product

  Delete Product

  Manage Users

  Publish Content

  View Analytics

Instead of:

  role

store:

  permissions

Schema:

  permissions

  roles

  role_permissions

This becomes:

  Role-Based Access Control

commonly called:

  RBAC

Used by:

Check out this video and this one to learn more about the RBAC and the more advanced (but extremely flexible and useful) ABAC. These are various ways to define permissions in a Software application.

Part 15 — Security Philosophy

❌ Never assume:

  Hidden Button = Security

Attackers can call:

  POST /products/delete/5

directly.

Always validate permissions On The Server!

Every protected action.

Every time.

No exceptions.

Common Beginner Mistakes

Protecting UI But Not Routes

Bad:

  Hide Delete Button

Attackers can still call:

  POST /delete

directly.

Trusting Role Values From Forms

Never.

Duplicating Permission Logic

Centralize it.

Hardcoding Admin IDs

Bad:

  if(user.id === 1)

Use roles.

Forgetting Ownership

Sometimes:

  User owns content

matters more than role.

Assignment

1) Add:

  role

column to users.

2) Implement:

  requireRole()

middleware.

3)

Restrict:

  Delete Product

to admins.

4)

Hide:

  Delete Button

for non-admins.

and

  Edit Button

for viewers and guests (non-logged-in users).

5)

Create:

  401.ejs
  403.ejs

error pages.


Bonus Challenge

Add:

  created_by

column to products.

When creating:

  created_by = req.user.id

Allow:

  Admins

to edit all products.

Allow:

  Editors

to edit only products they created.

Reject all others.

You have now implemented the foundations of content ownership used by many professional CMS platforms.

Key Takeaways

Today you learned:

The CMS now understands not only who users are, but also what they are allowed to do. This is one of the most important transitions from a simple application to a multi-user system suitable for real organizations.


⚠️ A large part of the content of this module was created using Generative AI (ChatGPT). The synthetic (AI-generated) content was reviewed and curated by Kostas Minaidis.


Project maintained by in-tech-gration Hosted on GitHub Pages — Theme by mattgraham