WardenAuthAgent Security
PricingDocsCompareBlogLearnChangelog
Sign inGet started free
All posts
Architecture
July 14, 2025
10 min read

Externalized Authorization: Why Access Logic Doesn't Belong in Your Application Code

Scattered permission checks are a maintenance and security liability. Externalized authorization separates the Policy Enforcement Point from the Policy Decision Point — here is the pattern, the latency objections answered, and a safe migration path.


Search your codebase for if (user.role === and count the hits. Every one is a business rule about who can do what, hard-coded into a handler, duplicated across services, and invisible to your security team. This is embedded authorization, and it does not scale. Externalized authorization is the fix.

The Embedded Authorization Anti-Pattern

Embedded authorization looks harmless in a single handler and catastrophic across a codebase:

typescript
// invoices.controller.ts
if (user.role !== 'admin' && user.role !== 'manager') throw new Forbidden()

// reports.controller.ts
if (!user.isAdmin && user.plan !== 'enterprise') throw new Forbidden()

// billing.service.ts
if (user.role === 'viewer') throw new Forbidden()  // slightly different rule!

The problems compound quickly:

  • Inconsistency. The same concept (“can manage billing”) is expressed three different ways.
  • No audit story. There is no single place to answer “who can delete invoices?”
  • Change friction. A policy change means a code change, a review, and a deploy — per service.

What Externalized Authorization Means

Externalized authorization separates two responsibilities inherited from the XACML reference architecture: the Policy Enforcement Point (PEP) — thin code in your app that asks “is this allowed?” — and the Policy Decision Point (PDP) — a dedicated service that owns the logic and answers. Your application stops knowing the rules and starts asking for decisions.

typescript
// The PEP is now identical everywhere — one line, no business logic:
async function requirePermission(userId, scopeId, resource, action) {
  const { allowed } = await pdp.hasAccess({ subjectId: userId, scopeId, resource, action })
  if (!allowed) throw new Forbidden()
}

// billing.service.ts
await requirePermission(user.id, scopeId, 'billing', 'manage')

Why It Wins

  • One source of truth. Rules live in the PDP; every service enforces the same decision.
  • Auditability. Policy changes are data changes, logged and reviewable — see our access policies model.
  • Velocity. Changing who can do what no longer requires a code deploy.

The Latency Objection

“But now every check is a network call.” This is the most common objection, and it is answerable. A well-designed PDP returns decisions in single-digit milliseconds, and request-scoped caching deduplicates repeated checks within a request. We cover the engineering in Engineering Sub-10ms Authorization Checks.

A Safe Migration Path

Use the strangler pattern — do not rewrite everything at once:

  • Introduce a single requirePermission helper backed by the PDP.
  • Replace embedded checks one endpoint at a time, starting with the highest-risk ones.
  • Mirror decisions (log-only) before enforcing, to catch behavioral differences safely.

How WardenAuth Fits

WardenAuth is an externalized PDP with a single has-access endpoint. Your services keep a one-line PEP; the roles, permissions, and deny-wins logic live in WardenAuth and are editable without a deploy. Try it free.


Back to blogTry WardenAuth free →
© 2026 ecarrizo. All rights reserved.
PricingDocsCompareBlogLearnChangelogStatusGlossaryContactTermsPrivacy