Skip to content
Back to Quality Assurance

Authentication & Authorization

Prove who the user is (authn) and decide what they may do (authz) — correctly, on every request.

Why

Authn and authz are the most security-critical code in any application and the most common source of breaches. A single endpoint that skips the authz check (because it was added late and the dev copied a public route) lets any user read any other user's data. The two are often conflated — "the user is logged in, so they can access this" — but login (authn) only proves identity; it says nothing about whether this user may access this specific resource. Correct authz is per-request and per-resource: the authenticated user must own or have permission for the specific object they are requesting. Getting this wrong at scale means a single bug exposes every user's data, not just one.

When

Design the authn/authz model before the first endpoint. Review every new route for correct authz before merge. Audit the full authz matrix quarterly — it is the code most likely to have silently drifted.

How

1. Separate authn (who) from authz (what) — never conflate them
   - Authn: verify the JWT/session cookie, load the user identity
   - Authz: check that this identity has permission for this specific resource

2. Authn: use a battle-tested library, do not roll your own
   - NextAuth/Auth.js, Passport, Keycloak, or your cloud's IAM (Cognito, Auth0)
   - Store password hashes with bcrypt(cost=12) or argon2id — never MD5/SHA1
   - Issue short-lived JWTs (15min) + refresh tokens, never permanent sessions

3. Authz: check per-request, per-resource, on the server
   - Every endpoint: load the resource, check resource.ownerId === user.id (or role check)
   - Never trust the client to filter — the client can be bypassed with a curl call
   - Use a middleware or decorator so the check cannot be forgotten

4. Test the authz matrix exhaustively
   - For each endpoint, test: owner can access, non-owner cannot, anonymous cannot
   - Use a parameterized test that iterates every route × every role
   - A single "non-owner gets 403" test per endpoint catches most IDOR bugs

5. Audit and rotate
   - Quarterly: review every route's authz, look for drift (a route that lost its check)
   - Rotate keys and secrets regularly; revoke sessions on password change
   - Log every authz denial — a spike in 403s may indicate an attack or a bug

Example

// src/middleware/authz.ts — per-request, per-resource authorization
import type { Request, Response, NextFunction } from "express";

export function requireOwnership(getResourceId: (req: Request) => string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.id;       // set by authn middleware
    if (!userId) return res.status(401).json({ error: "Unauthorized" });

    const resourceId = getResourceId(req);
    const resource = await db.resources.findById(resourceId);
    if (!resource) return res.status(404).json({ error: "Not found" });

    if (resource.ownerId !== userId && req.user.role !== "admin") {
      return res.status(403).json({ error: "Forbidden" }); // non-owner denied
    }
    req.resource = resource;
    next();
  };
}

// route: only the owner can delete their resource
router.delete("/resources/:id", requireOwnership((r) => r.params.id), async (req, res) => {
  await db.resources.delete(req.resource.id);
  res.status(204).end();
});

// test: non-owner gets 403
it("denies non-owner", async () => {
  const res = await request(app).delete("/resources/42").set("auth", tokenForUser(99));
  expect(res.status).toBe(403); // 42 is owned by user 1, not 99
});

Common Problems

  • ⚠Checking only that the user is logged in (authn) and not that they own the specific resource (authz) — this is the classic IDOR vulnerability that lets user A read user B's data by changing an ID in the URL.
  • ⚠Doing authz on the client (hiding the edit button if the user is not the owner) but not on the server — the server endpoint is unprotected, and anyone with a curl command can edit any resource.
  • ⚠Using a role check (isAdmin) where an ownership check is needed — an admin-only gate blocks non-admins but lets any admin edit any resource, which is wrong if admins should only read, not mutate.
  • ⚠Forgetting the authz check on a new endpoint because it was added by copying a public route — the route ships unprotected, and the bug is found by an attacker or a lucky user, not by the team.

Best Practices

  • ✓Enforce authz in a single middleware or decorator that every protected route must use — make it impossible to add a route without the check by linting for unprotected handlers.
  • ✓Test every endpoint with three cases: owner succeeds, non-owner gets 403, anonymous gets 401 — a parameterized test over all routes catches the one route that lost its check.
  • ✓Load the resource and check ownership on the server, never trust the client — the client is an optimization (hide the button), the server is the security boundary.
  • ✓Log every 403 with the user ID, resource ID, and action, and alert on spikes — a sudden increase in denials may indicate an attack probing for IDOR or a bug in a new route.