Lesson 20 / 25

Enforcing Authorization in APIs

Object-level checks and policy engines.

Check every object, deny by default

The most common API flaw is broken object level authorization (BOLA): GET /orders/123 returns order 123 to anyone logged in because the handler never checks ownership. Enforce authorization server-side on every request, for every object, defaulting to deny. Prefer scoping queries by the caller (WHERE id = ? AND account_id = ?) so unauthorised rows are never loaded, and check function-level permissions (admin routes) and property-level rules (who may set role or price). Centralise decisions in one module or a policy engine so rules are consistent and testable: Open Policy Agent (OPA) evaluates policies written in Rego; Cedar is a policy language from AWS used by Amazon Verified Permissions. Engines decide; your code must still call them and act on the answer.

Scoped queries plus a central policy check

Express-style TypeScript; can() is your policy module or engine client.

// central decision point: easy to test and audit
export function can(user: User, action: string, resource: { ownerId: string; accountId: string }) {
  if (user.accountId !== resource.accountId) return false;        // tenant boundary
  if (action === 'order:read') return user.id === resource.ownerId || user.roles.includes('support');
  if (action === 'order:refund') return user.roles.includes('finance');
  return false;                                                    // deny by default
}

app.post('/orders/:id/refund', requireUser, async (req, res) => {
  // scope by tenant in the query itself
  const order = await db.orders.findOne({ id: req.params.id, accountId: req.user.accountId });
  if (!order) return res.status(404).end();
  if (!can(req.user, 'order:refund', order)) return res.status(403).end();
  await refunds.create(order, { amount: order.total }); // amount from server data, not the client
  res.status(202).end();
});

Test authorization like a feature

Write tests that log in as user A and request user B's objects, for every endpoint. Authorization bugs rarely show up in happy-path tests.

Quick check: An API returns any invoice by ID to any logged-in user. What is this flaw called?

  • Session fixation
  • Broken object level authorization (BOLA / IDOR)
  • Credential stuffing
  • Algorithm confusion
Answer

Broken object level authorization (BOLA / IDOR) — The handler authenticates but never checks ownership of the object.