Authorization Best Practices

Chapter 1 · Practice 3 of 3

Grant the least privilege and fail closed on errors

From Deny by default, check on the server

Why it matters

Least privilege means each user, service and API key gets only what its job needs. OWASP applies it "both horizontally and vertically": horizontally across peers (a user sees their own records, not a colleague's) and vertically across levels (a member cannot reach admin functions). It also recommends periodic reviews to catch privilege creep, where people collect permissions as they change teams and never lose them.

Failing closed is the other half. OWASP's cheat sheet asks you to "ensure all exception and failed access control checks are handled no matter how unlikely they seem". If the permission lookup throws, times out or returns something unexpected, the answer is no:

  • Wrap checks so that only an explicit allowed: true grants access.
  • Do not use catch blocks that log and continue.
  • Treat a missing session, missing tenant or missing resource the same way: deny, then return the same response.

The OWASP Top 10:2025 made failing open its own category (A10, Mishandling of Exceptional Conditions), which is a good reminder that authorization code is error handling code.

Example

TypeScript
export async function requirePermission(user: User, action: string, resource: Resource) {
  let allowed = false;
  try {
    allowed = (await policy.evaluate({ user, action, resource })) === true;
  } catch (err) {
    log.error({ err, userId: user.id, action }, 'policy evaluation failed');
  }
  if (!allowed) throw new ForbiddenError(); // the only way past is an explicit yes
}