Skip to main content

🛂 Authentication vs Authorization

Week 9: Authentication & Security — course module banner illustration

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 — 401 vs 403 — for each failure
  • Implement an Express protect middleware 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.

graph TD A[Incoming Request] --> B[Authentication] B --> C{Who are you?} C -->|Verified| D[Authorization] C -->|Unknown| E[401 Unauthorized] D --> F{What can you do?} F -->|Permitted| G[Access Granted] F -->|Forbidden| H[403 Forbidden]

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.

FactorMeaningExamples
Something you knowA secret in your headPassword, PIN, passphrase
Something you haveA physical or digital tokenPhone (SMS/authenticator app), security key
Something you areA biometric traitFingerprint, 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.
sequenceDiagram participant User participant Client as Browser participant Server participant DB as Database User->>Client: Enter email and password Client->>Server: POST /login Server->>DB: Look up user by email DB-->>Server: User record with password hash Server->>Server: Compare submitted password to hash alt Password matches Server-->>Client: Set session or token Client-->>User: Show authenticated app else Password wrong or no user Server-->>Client: 401 and generic error Client-->>User: Show "Invalid credentials" end

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.

Authorization: Granting Access

Authorization (or authz) begins only after authentication succeeds. Now that the system knows who you are, it decides what you may see and do. It answers "Are you allowed to do this?"

Common authorization models

  • RBAC — Role-Based Access Control: permissions attach to roles (admin, editor, user), and users are assigned roles. Simple and by far the most common.
  • ABAC — Attribute-Based Access Control: decisions use attributes of the user, resource, and context (department, time of day, document owner).
  • Ownership checks: object-level rules like "you may edit a post only if you wrote it." Critical, and easy to forget.

🏨 Analogy: hotel key cards

Everyone who checks in gets a key card — that card authenticates them as a guest. But the card's encoded permissions authorize different doors: a standard card opens only your room, a staff card opens service areas, a manager card opens almost everything. Same building, same "logged in" state, very different access.

Role-based authorization middleware

This factory returns a middleware that only lets certain roles through. It assumes protect already ran and set req.user.

// Authorization middleware factory: gates a route by role.
// Usage: router.delete('/:id', protect, authorize('admin'), deleteUser)
function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!allowedRoles.includes(req.user.role)) {
      // Identity is known, but this role lacks permission -> 403, not 401.
      return res.status(403).json({
        message: 'You do not have permission to perform this action'
      });
    }
    next();
  };
}

module.exports = { authorize };

Ownership (object-level) authorization

Role checks alone are not enough. A logged-in user shouldn't edit another user's post just because both are "users." Check ownership on the specific resource:

// Inside an "update post" controller, after loading the post:
async function updatePost(req, res) {
  const post = await Post.findById(req.params.id);
  if (!post) {
    return res.status(404).json({ message: 'Post not found' });
  }

  // Ownership check: owner OR admin may proceed.
  const isOwner = post.user.toString() === req.user.id;
  if (!isOwner && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Not allowed to edit this post' });
  }

  post.title = req.body.title ?? post.title;
  await post.save();
  res.json({ data: post });
}

Why it matters: the single most common real-world access-control bug — Insecure Direct Object Reference (IDOR) — is exactly this check being missing. The endpoint authenticates you, then trusts the ID in the URL without asking whether the resource is yours.

Key Differences & Status Codes

AspectAuthenticationAuthorization
Question answered"Who are you?""What can you do?"
RunsFirstAfter authentication
Based onCredentials, tokens, biometricsRoles, policies, ownership
Visible to user?Yes — you log inUsually invisible until denied
Failure code401 Unauthorized403 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;
A request passes through authentication, then authorization, before reaching the resource Request with token Authn who are you? Authz what can you do? Resource 200 OK fail → 401 fail → 403
Identity is established once, then every downstream check assumes it. A failure in the first box never reaches the second.

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.

  1. A request arrives with no token at all.
  2. A logged-in user tries to hit an admin-only route.
  3. A token has expired.
  4. 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) before authorize (authz), always in that order
  • Enforce every permission on the server, even if the UI already hides it
  • Return 401 for missing/invalid identity, 403 for 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 401 for unknown/invalid identity, 403 for denied permission
  • Enforce both on the server, on every sensitive endpoint
  • Add ownership checks to defeat IDOR — roles alone are not enough

📚 Additional Resources

🚀 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.