User Authentication and Authorization

Authentication = who the user is (login)
Authorization = what the user can access (permissions/roles)

Below is a complete, course-style guide for implementing auth in a Node.js + Express + MongoDB (MERN backend), including hashing, JWT, middleware, RBAC, and cookies/sessions.


Implementing User Registration and Login

Registration flow

  1. User submits name/email/password
  2. Backend validates input
  3. Password is hashed
  4. User is stored in MongoDB
  5. Optionally: auto-login by issuing token/cookie

User schema (Mongoose)

import mongoose from "mongoose";

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true, trim: true },
    email: { type: String, required: true, unique: true, lowercase: true, trim: true },
    passwordHash: { type: String, required: true },
    role: { type: String, enum: ["user", "admin"], default: "user" }
  },
  { timestamps: true }
);

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

Login flow

  1. User submits email/password
  2. Backend finds user by email
  3. Compare password with stored hash
  4. If valid → issue session or JWT
  5. Return success + user info (never return passwordHash)

Password Hashing and Storing in MongoDB

Why hashing?

Passwords must never be stored as plain text. Hashing protects users even if the database leaks.

Use bcrypt:

  • slow hashing = harder to crack
  • includes salting automatically

Install:

npm i bcrypt

Hash password during registration

import bcrypt from "bcrypt";
import { User } from "./models/User.js";

export async function register(req, res) {
  const { name, email, password } = req.body;

  if (!name || !email || !password || password.length < 8) {
    return res.status(400).json({ success: false, error: "Invalid input" });
  }

  const existing = await User.findOne({ email });
  if (existing) {
    return res.status(409).json({ success: false, error: "Email already registered" });
  }

  const saltRounds = 12;
  const passwordHash = await bcrypt.hash(password, saltRounds);

  const user = await User.create({ name, email, passwordHash });

  res.status(201).json({
    success: true,
    user: { id: user._id, name: user.name, email: user.email, role: user.role }
  });
}

Compare password during login

export async function login(req, res) {
  const { email, password } = req.body;

  const user = await User.findOne({ email });
  if (!user) return res.status(401).json({ success: false, error: "Invalid credentials" });

  const ok = await bcrypt.compare(password, user.passwordHash);
  if (!ok) return res.status(401).json({ success: false, error: "Invalid credentials" });

  res.json({ success: true, message: "Login ok" });
}

Protecting Routes with Authentication Middleware

There are two common approaches:

Approach A: JWT (Token-based auth)

  • Backend issues a JWT
  • Frontend sends it in Authorization: Bearer <token>
  • Backend verifies token on protected routes

Install:

npm i jsonwebtoken

Create token

import jwt from "jsonwebtoken";

function signToken(user) {
  return jwt.sign(
    { userId: user._id.toString(), role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: "1h" }
  );
}

Auth middleware (JWT)

export function requireAuth(req, res, next) {
  const header = req.headers.authorization;

  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ success: false, error: "Missing token" });
  }

  const token = header.split(" ")[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload; // { userId, role, iat, exp }
    next();
  } catch {
    return res.status(401).json({ success: false, error: "Invalid/Expired token" });
  }
}

Protect route

app.get("/api/profile", requireAuth, async (req, res) => {
  res.json({ success: true, user: req.user });
});

Approach B: Sessions (Cookie-based auth)

  • Server stores session data
  • Browser stores session id cookie
  • Easy for classic web apps, also works for SPAs

Install:

npm i express-session connect-mongo

Setup:

import session from "express-session";
import MongoStore from "connect-mongo";

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({ mongoUrl: process.env.MONGO_URI }),
  cookie: {
    httpOnly: true,
    secure: false,   // true in production with HTTPS
    sameSite: "lax",
    maxAge: 1000 * 60 * 60 * 24 // 1 day
  }
}));

Set session on login:

req.session.user = { userId: user._id.toString(), role: user.role };
res.json({ success: true });

Middleware:

export function requireSession(req, res, next) {
  if (!req.session?.user) {
    return res.status(401).json({ success: false, error: "Not logged in" });
  }
  req.user = req.session.user;
  next();
}

Role-Based Access Control (RBAC)

What is RBAC?

RBAC means users have roles like:

  • user
  • admin
  • manager

and routes/actions are allowed based on those roles.

Store role in DB

We already added role field in the User schema.

RBAC middleware

export function requireRole(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user?.role || !allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ success: false, error: "Forbidden" });
    }
    next();
  };
}

Admin-only route

app.delete("/api/admin/users/:id",
  requireAuth,
  requireRole("admin"),
  async (req, res) => {
    // delete user logic
    res.json({ success: true });
  }
);

Session Management and Cookies

Cookies basics

Cookies are small key-value data stored in the browser and automatically sent with requests to the same domain.

Types:

  • HttpOnly cookies: not accessible via JavaScript (more secure)
  • Secure cookies: sent only over HTTPS
  • SameSite: controls cross-site sending (CSRF protection)

JWT in HttpOnly cookie (recommended for production SPAs)

This combines token auth with cookie security.

Set cookie:

res.cookie("token", token, {
  httpOnly: true,
  secure: false,      // true in production with HTTPS
  sameSite: "lax",
  maxAge: 60 * 60 * 1000
});

Read cookie token:

  • Use cookie-parser

Install:

npm i cookie-parser
import cookieParser from "cookie-parser";
app.use(cookieParser());

export function requireAuthCookie(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ success: false, error: "Missing token" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ success: false, error: "Invalid token" });
  }
}

CORS + cookies (important for React + Express on different ports)

If using cookies across origins:

  • React must send credentials: "include"
  • Express must enable CORS with credentials

React:

fetch("http://localhost:5000/api/profile", {
  credentials: "include"
});

Express:

import cors from "cors";
app.use(cors({
  origin: "http://localhost:3000",
  credentials: true
}));

Best Practices (must-follow)

  • Never store plain passwords
  • Use bcrypt with 10–12+ rounds
  • Store secrets in .env
  • Use short token expiry + refresh tokens for long sessions (advanced)
  • Prefer HttpOnly cookies in production
  • Add rate limiting for login endpoints
  • Validate inputs (use zod/joi/express-validator)
  • Always return generic auth errors (“Invalid credentials”)

Summary

  • Registration/Login: validate → hash password → store user → authenticate
  • Password storage: bcrypt hash in MongoDB (passwordHash)
  • Route protection: middleware verifies JWT or session
  • RBAC: restrict endpoints by user roles
  • Session & cookies: HttpOnly cookies improve security; sessions store state on server

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *