Security Deep Dive

Authorization — What Are You Allowed to Do?

Authentication tells you who the request is from. Authorization decides what they're allowed to do. It's the layer where most real-world breaches happen — usually because some endpoint forgot to check.

RBACABACReBACLeast PrivilegePolicies
← Back to Security
Quick Facts

At a Glance

Basic Concepts

  • Subject: who is acting (user, service account, API client).
  • Resource: what they're acting on (a link, an order, a tenant).
  • Action: what they're trying to do (read, update, delete, share).
  • Policy: the rule that decides whether subject may perform action on resource.
  • Least privilege: grant the minimum permissions needed; nothing more.
  • Default deny: if no rule explicitly allows it, reject.
Models

Three Ways to Think About Permissions

ModelDecisions based on…Best for
RBAC — Role-BasedThe user's role(s): admin, editor, viewer.Most apps. Simple, auditable, easy to reason about.
ABAC — Attribute-BasedAttributes of subject, resource, and environment (tenant, time, IP, sensitivity).Complex enterprise rules ("editors can publish during business hours from corp IPs").
ReBAC — Relationship-BasedGraph relationships ("is this user a member of the team that owns this doc?").Sharing-heavy apps — Google Docs, GitHub. Inspired by Google's Zanzibar.

In practice, mature systems blend all three: RBAC for coarse roles, ABAC for context, ReBAC for sharing.

RBAC in Practice

Roles, Permissions, and the Trap

  • Permissions are verbs on resources: link:read, link:delete, user:invite.
  • Roles bundle permissions. editor = read + create + update.
  • Users have roles (often per-tenant). Avoid one global role per user — that's how you end up with "support engineer" who needs prod access in one tenant only.
  • The trap: proliferation. After a few years you have 47 overlapping roles nobody can audit. Periodically review and consolidate.
Where to Check

Layers of Authorization

Edge / API gateway

Coarse checks: is the token valid, does it have the required scope, is the rate limit OK. Cheap and fast — keeps obvious garbage out.

Application / route

"Is this user allowed to call this endpoint at all?" — usually role-based middleware. Easy to centralize, easy to bypass if you skip it.

Object-level (the critical one)

"Is this user allowed to act on this specific record?" Skipping this is the #1 OWASP issue: Broken Access Control →.

An "editor" role doesn't mean "edit any link" — it means "edit links you own." Always check ownership/relationship at the object level.

Data layer

Last line of defense. Postgres row-level security (RLS) or per-tenant DB schemas keep the wrong rows from leaving the server even if a query forgets a WHERE owner_id = ?.

Common Vulnerabilities

Three Bugs You Will Write Eventually

IDOR — Insecure Direct Object Reference

GET /api/links/42 returns link 42 to anyone who knows the URL — no ownership check. The attacker just iterates IDs.

Fix: always join on owner_id = current_user_id in the query. Use unguessable IDs (UUID v7, ULID) as defense in depth, never as the only defense.

Privilege escalation via mass assignment

PATCH /users/me {"role": "admin"} — and the framework happily writes role because it's a column on the model.

Fix: explicit allow-lists of fields per endpoint. Never deserialize directly into the entity.

Forgotten checks on side-doors

The main UI checks ownership. The CSV export endpoint, the GraphQL field resolver, the admin-only debug route — all written by different people, all forgot.

Fix: centralize the policy. Have one can(user, action, resource) function and call it everywhere. Add an integration test for every endpoint.

Centralizing Policy

Policy Engines & Policy-as-Code

When rules get complex, scattering them through controllers is a maintenance disaster. Move policies into a dedicated layer.

ToolWhat it offers
OPA / RegoGeneral-purpose policy language; runs as a sidecar or library. Defacto standard.
CedarAWS-developed policy language; designed for ABAC + RBAC blends.
OpenFGA / Permify / Authzed (SpiceDB)Zanzibar-style ReBAC stores. Model permissions as a graph.
Casbin / OsoEmbeddable libraries — policies live alongside your code.
Postgres RLSRow-level security in the database itself. Underrated default for multi-tenant apps.

You don't need a policy engine on day one. You do need a single function the whole codebase calls.

Worked Example

The URL Shortener Authorization Layer

A small policy with three roles, ownership checks, and a rate-limited anonymous quota.

// One function. Call it everywhere.
function can(user, action, link) {
  if (action === 'link:read') return true;          // public redirects
  if (!user) return false;                          // everything else needs auth

  if (user.role === 'admin') return true;           // admins can do anything

  if (action === 'link:create')  return true;       // any user can create
  if (action === 'link:delete' || action === 'link:update')
    return link.owner_id === user.id;               // ownership check

  return false;                                     // default deny
}

// Usage in the route handler
app.delete('/links/:code', requireAuth, async (req, res) => {
  const link = await db.findLinkByCode(req.params.code);
  if (!link) return res.sendStatus(404);
  if (!can(req.user, 'link:delete', link)) return res.sendStatus(403);
  await db.deleteLink(link.id);
  res.sendStatus(204);
});

Pair this with a Postgres policy: CREATE POLICY links_owner ON links USING (owner_id = current_setting('app.user_id')::bigint);. Now even a query bug can't leak someone else's links.

Testing It

How to Know It Works

  • Negative tests are mandatory. For every endpoint, write "user A cannot access user B's resource" — and watch it fail before you fix it.
  • Tabletop the matrix. Roles × actions × resource states. Spot-check the surprising cells.
  • Audit logs: log every authorization decision (allow + deny). You'll need them after an incident.
  • Periodic review: dump the role/permission graph quarterly. Look for "admin-equivalent" roles you didn't intend.
Continue

Related Reading