🛂 Authentication vs Authorization
They sound alike, they're often used in the same sentence, and mixing them up is the root cause of a surprising number of security holes. Authentication answers "who are you?" — authorization answers "what are you allowed to do?" Today you'll learn to tell them apart cold, and wire both into a real Express API.
Week 9 · Day 1 (Monday: Authentication Basics) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define authentication and authorization and explain precisely how they differ
- List the three factors of authentication and common web auth strategies (session, JWT, OAuth)
- Choose the correct HTTP status code —
401vs403— for each failure - Implement an Express
protectmiddleware that verifies identity - Implement role-based and ownership-based authorization checks
- Recognize common pitfalls like IDOR and client-side-only access control
Estimated Time: 55 minutes
Practice: Build a role-guard middleware and reason through five access-control scenarios.
In This Lesson
Two Guards at the Gate
Picture a gated community with two security guards. The first stands at the entrance and checks your ID against your face — that's authentication, verifying you are who you claim to be. Once you're inside, a second guard checks whether your resident badge lets you into the pool, the gym, or the maintenance shed — that's authorization, deciding what you're permitted to do. You always meet the first guard before the second: you can't ask "what may this person access?" until you know who the person is.
This lesson is the foundation for the entire security week. Password hashing, JSON Web Tokens, sessions, and role systems all sit on top of this one distinction. Get it wrong and you'll build a house whose front door checks IDs but whose interior rooms swing open for anyone.
Authentication: Proving Identity
Authentication (often shortened to authn) is the process of verifying a claimed identity. The system asks, "Are you really who you say you are?" and demands evidence before it believes you.
The three factors
Every authentication method boils down to one or more of these categories. Combining two or more is multi-factor authentication (MFA), which is dramatically stronger than any single factor.
| Factor | Meaning | Examples |
|---|---|---|
| Something you know | A secret in your head | Password, PIN, passphrase |
| Something you have | A physical or digital token | Phone (SMS/authenticator app), security key |
| Something you are | A biometric trait | Fingerprint, face scan, voice |
✈️ Analogy: airport check-in
At the airport you present your ID and boarding pass. An officer compares your face to your photo and validates the pass. Once satisfied, you're waved through security. In a web app the "boarding pass" you receive afterward is a session cookie or a token — proof you've already been vetted, so you don't re-enter your password on every request.
Common web authentication strategies
- Session-based — the server stores session state and hands the browser a session ID in a cookie. Stateful; the server must remember every active session.
- Token-based (JWT) — the server issues a signed token holding user claims. Stateless; the server verifies the signature instead of looking anything up. (You'll build these next.)
- OAuth / social login — you delegate identity to a trusted provider ("Sign in with Google"), receiving access without ever handling the user's Google password.
Verifying identity in Express
Here is a focused authentication middleware. Its only job is to answer "who is this?" — it attaches the verified user to the request and stops here if it can't. Notice it never decides permissions; that comes later.
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// Authentication middleware: establishes WHO the caller is.
async function protect(req, res, next) {
let token;
// Accept a Bearer token from the Authorization header...
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.split(' ')[1];
} else if (req.cookies?.token) {
// ...or a token stored in an httpOnly cookie.
token = req.cookies.token;
}
if (!token) {
// No proof of identity at all.
return res.status(401).json({ message: 'Not authenticated' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Attach the user (never the password hash) for later middleware.
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
next();
} catch (err) {
// Bad signature, expired token, tampered payload — all mean "unknown".
return res.status(401).json({ message: 'Not authenticated' });
}
}
module.exports = { protect };
Why it matters: keeping authentication in its own single-purpose middleware means every protected route reuses the exact same identity check. There's one place to audit, one place to fix, and no route can accidentally skip it.
Key Differences & Status Codes
| Aspect | Authentication | Authorization |
|---|---|---|
| Question answered | "Who are you?" | "What can you do?" |
| Runs | First | After authentication |
| Based on | Credentials, tokens, biometrics | Roles, policies, ownership |
| Visible to user? | Yes — you log in | Usually invisible until denied |
| Failure code | 401 Unauthorized | 403 Forbidden |
| Failure message | "Invalid credentials" | "Insufficient permissions" |
⚠️ The 401 naming trap
Despite its label, HTTP 401 Unauthorized is really an authentication failure — "we don't know who you are, please log in." 403 Forbidden is the true authorization failure — "we know exactly who you are, and you're not allowed." Use 401 when identity is missing or invalid, 403 when a known user lacks permission.
How They Work Together
In a well-built request pipeline the two layers are chained, in order, and both run on the server. A typical protected route looks like this:
const express = require('express');
const { protect, authorize } = require('../middleware/auth');
const { getUsers, deleteUser } = require('../controllers/users');
const router = express.Router();
// 1) protect -> authentication (who are you?)
// 2) authorize-> authorization (are you an admin?)
router.get('/', protect, authorize('admin'), getUsers);
router.delete('/:id', protect, authorize('admin'), deleteUser);
module.exports = router;
Never trust the client. A frontend can hide an "Admin" button, but that's cosmetic — anyone can call your API directly with curl. Authorization must be enforced on the server, on every sensitive endpoint, every time.
Practice & Quiz
🏋️ Exercise 1: An ownership guard
Goal: Write canModify(user, resource) that returns true when the user owns the resource or is an admin, and false otherwise. Assume resource.ownerId and user.id are strings, and user.role is 'user' or 'admin'.
function canModify(user, resource) {
// TODO: return true if the user owns the resource or is an admin
}
console.log(canModify({ id: 'a1', role: 'user' }, { ownerId: 'a1' })); // true
console.log(canModify({ id: 'a1', role: 'user' }, { ownerId: 'zz' })); // false
console.log(canModify({ id: 'a1', role: 'admin' }, { ownerId: 'zz' })); // true
💡 Hint
Compare resource.ownerId to user.id for ownership, then allow an escape hatch when user.role === 'admin'. Combine both with ||.
✅ Solution
function canModify(user, resource) {
const isOwner = resource.ownerId === user.id;
return isOwner || user.role === 'admin';
}
This mirrors the object-level check you saw in the controller. In production you'd still return 403 from the route when it yields false.
🏋️ Exercise 2: Pick the status code
Goal: For each scenario, decide whether the correct response is 401 or 403.
- A request arrives with no token at all.
- A logged-in
usertries to hit anadmin-only route. - A token has expired.
- A user tries to delete someone else's comment.
✅ Solution
- 1 →
401(no identity) - 2 →
403(known user, wrong role) - 3 →
401(identity can no longer be verified) - 4 →
403(known user, not the owner)
🎯 Quick Quiz
Question 1: Which always happens first?
Question 2: A known, logged-in user lacks permission for an action. Which status code fits?
Question 3: Where must authorization checks ultimately be enforced?
Best Practices & Pitfalls
✅ Do
- Run
protect(authn) beforeauthorize(authz), always in that order - Enforce every permission on the server, even if the UI already hides it
- Return
401for missing/invalid identity,403for denied permission - Add ownership checks on any route that mutates a specific resource
- Follow the principle of least privilege — grant the minimum access needed
- Centralize auth logic in reusable middleware so nothing gets skipped
❌ Don't
- Trust a role or user ID sent in the request body — derive it from the verified token
- Expose a resource by ID without asking "is this caller allowed to see it?" (IDOR)
- Leak why a login failed ("wrong password" vs "no such user") — that enables user enumeration
- Rely on the frontend to keep admin routes safe
- Confuse "logged in" with "allowed" — authentication is not authorization
⚠️ IDOR in one sentence
If GET /api/invoices/1043 returns invoice 1043 to any logged-in user without checking that they own it, you have an Insecure Direct Object Reference — the classic authorization failure. Authentication passed; authorization was never done.
Summary
🎉 Key Takeaways
- Authentication = "who are you?"; authorization = "what can you do?"
- Authentication always runs first; authorization builds on its result
- Use
401for unknown/invalid identity,403for denied permission - Enforce both on the server, on every sensitive endpoint
- Add ownership checks to defeat IDOR — roles alone are not enough
📚 Additional Resources
- OWASP — Authentication Cheat Sheet
- OWASP — Authorization Cheat Sheet
- MDN — 401 Unauthorized
- MDN — 403 Forbidden
🚀 What's Next?
Authentication depends on comparing a submitted password to a stored one — but you must never store the password itself. Next up: Password Hashing with bcrypt, where you'll learn to store credentials so that even a full database breach doesn't hand attackers your users' passwords.
🔐 Great start to security week!
You can now tell authentication from authorization in your sleep — the mental model every secure feature is built on.