Authorization Best Practices

Chapter 1 · Practice 1 of 3

Deny by default and require an explicit allow

From Deny by default, check on the server

Why it matters

OWASP's Authorization Cheat Sheet says an application "should be configured to deny access by default", and the Top 10:2025 lists "deny access by default, except for public resources" first among its prevention steps for A01.

In code, that means the absence of a rule is a denial:

  • A new route without a permission annotation is rejected, not public.
  • A role with no grant for an action cannot do it.
  • An unknown resource type, action or attribute falls through to "deny".

Frameworks help or hurt here. Some make every route public until you add a guard. Flip that: install a global guard that denies, and mark the few public routes explicitly (@Public(), an allowlist of paths, a public: true flag). A forgotten annotation then fails safe, and a reviewer sees every public route in one place.

The same applies to data. A query helper that returns all rows unless you remember to filter is deny-by-default turned upside down. Prefer helpers that require a user or tenant argument (see the object-level chapter).

Example

TypeScript
// Every route needs a policy; routes without one are denied, not open
const routes = {
  'GET /health': { public: true },
  'GET /projects/:id': { action: 'project:read' },
  'POST /projects/:id/archive': { action: 'project:archive' },
} as const;

export function guard(routeKey: string, user: User | null, resource?: unknown) {
  const rule = routes[routeKey as keyof typeof routes];
  if (!rule) return false; // unknown route: deny
  if ('public' in rule) return true;
  return user !== null && can(user, rule.action, resource);
}