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
- User submits name/email/password
- Backend validates input
- Password is hashed
- User is stored in MongoDB
- 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
- User submits email/password
- Backend finds user by email
- Compare password with stored hash
- If valid → issue session or JWT
- 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:
useradminmanager
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
Leave a Reply