Structuring Authorization: Authentication, Context, Policy, and Enforcement

•By FNB Solutions Engineering Team

Introduction

Authorization becomes difficult to maintain when it is implemented as a collection of local checks.
One endpoint verifies that the user is active. Another checks a role. A background operation checks ownership. A resource handler loads an object by identifier and assumes that authentication is sufficient.
Each individual decision may appear reasonable, but the overall authorization model becomes distributed across the application. Over time, this makes the system difficult to audit and increases the likelihood that one execution path will omit an important condition.

A more robust design is to separate authorization into distinct layers:
Authentication context : who is making the request?
Authorization context : what relevant facts are known about the actor and resource?
Policy decision : is the actor allowed to perform this action?
Enforcement : how is denial represented in the current execution context?

The purpose of this separation is not abstraction for its own sake. It is to ensure that authorization rules have a defined owner, can be evaluated consistently, and are not reconstructed independently throughout the application. This is particularly important for preventing object-level authorization failures, including IDOR vulnerabilities.


Architectural Overview

A typical protected operation moves through several distinct stages. The application first authenticates the actor and establishes who is making the request. It then gathers the authorization context, including any facts relevant to the decision, such as account state, roles, organization memberships, or entitlements. If the operation concerns a specific object, the application also identifies or loads that resource and any attributes needed to evaluate access. The policy layer then determines whether the actor is allowed to perform the requested action on that resource. Finally, the enforcement layer applies the result of that decision by allowing the operation to continue or by returning an error, redirecting the user, or otherwise terminating the request.

The central design principle is that callers should not assemble authorization rules from low-level predicates. They should request a policy decision. For example:

text
canAccessSubscriberContent(actor, now) canViewProject(actor, project) canEditUser(actor, targetUser) canReadInvoice(actor, invoice)

These names describe application-level capabilities. The implementation details remain inside the policy layer.


Authentication Establishes Identity

Authentication should answer one question: Who is making this request?
A session, token, certificate, or other authentication mechanism may establish a trusted identity. Its responsibility ends there.
Authentication does not establish that the actor may access a particular resource.

A successfully authenticated actor may still be:

  • suspended;
  • outside an entitlement period;
  • not the owner of the requested resource;
  • missing the required permission for the requested action.
    This distinction is fundamental.

An authenticated request is not necessarily an authorized request.
A system that treats successful authentication as sufficient protection can still expose data across users, organizations, or tenants.


Authorization Context Gathers Facts

After identity is established, the application gathers the facts needed to evaluate authorization. Depending on the operation, this may include the actor’s account status, global role, organization or tenant memberships, subscription state, entitlements, ownership of the target resource, the resource’s organizational scope, and environmental information such as the current time. The authorization context is responsible for assembling these facts into a form that the policy layer can evaluate, but it does not determine whether access should be granted. This keeps data retrieval separate from authorization logic: the context layer establishes what is true, while the policy layer determines what those facts mean for the requested operation. In a small system, a single authorization context may be sufficient, while larger systems may use narrower context loaders for administrative access, subscriptions, organization membership, or resource-specific decisions. The implementation can vary as the application grows without changing the underlying architectural boundary.


Policies Express Authorization Rules

Policies contain the business rules that determine whether an operation is allowed. A subscriber-access policy, for example, might require the account to be active and not deleted while allowing access only to administrators or users with a currently valid entitlement. A project policy might require the actor to be active, belong to the organization that owns the project, and hold the permission required for the requested action.

The important property is that the complete rule is defined in one place. Without a policy layer, callers tend to reconstruct authorization from smaller predicates such as isActive, isAdmin, isOwner, hasEntitlement, or isOrganizationMember. Those predicates may be correct individually, but every caller must still know how to combine them correctly.

That is where inconsistency appears. One path may enforce account state, tenant membership, and role correctly, while another omits one of those conditions or fails to confirm that the target resource belongs to the same tenant. The problem is often not an incorrect predicate, but the repeated assembly of the overall rule in different places.

A policy-oriented design moves that composition into a single, named decision. Instead of requiring every caller to understand how project access is calculated, the application exposes a policy such as canViewProject(actor, project). Callers supply the relevant context and enforce the result, while the underlying rule can evolve without requiring every execution path to be independently rewritten or re-audited.


Object-Level Authorization and IDOR

IDOR vulnerabilities occur when an application accepts a reference to an object but fails to verify that the current actor is authorized to access that specific object. For example, a user might request /invoices/1001, change the identifier to /invoices/1002, and receive the second invoice because the server checks only that the object exists, not whether the user may access it.

The underlying problem is not the use of a visible or sequential identifier. Sequential IDs can make enumeration easier, but replacing them with UUIDs does not solve the authorization issue. A resource identifier tells the application which object is being requested; it does not establish that the requester has permission to access it.

Object identification and authorization should therefore be treated as logically separate concerns. The application must identify the target resource and independently determine whether the current actor may perform the requested action on it. Possession of a valid identifier must never be treated as proof of access.


Why Centralized Policies Reduce IDOR Risk

A common IDOR-prone pattern is to authenticate the actor, load an object by its identifier, and return it without evaluating the relationship between the actor and that object. The request is authenticated, but the resource access is not authorized.

A safer design makes object-level authorization an explicit part of the operation. For a project, a decision such as canViewProject(actor, project) might consider the actor’s status, role, organization membership, and the project’s ownership, organization, or current state.

Centralizing that decision reduces the chance that different endpoints, services, or background jobs will apply different definitions of access. It does not make IDOR impossible—any code path that bypasses authorization can still introduce a vulnerability—but it makes object-level authorization a defined and expected part of resource access rather than an optional check implemented independently by each caller.


Authorization-Scoped Resource Access

For straightforward ownership and tenancy rules, authorization can also be enforced when the resource is loaded. Rather than retrieving a project solely by its identifier and checking access afterward, the query can be constrained to the actor’s authorized scope. A lookup might require both the requested project ID and membership in one of the actor’s permitted organizations or, in a simpler ownership model, require that the project’s ownerId match the current actor.

This has an important property: resources outside the actor’s authorization boundary are never returned by the query. For many object-level access patterns, that is a strong defense against IDOR because the application does not first load an arbitrary object and rely entirely on a later check.

Resource scoping and policy evaluation serve different purposes, however. A scoped query is well suited to determining which resources an actor may access, while a policy can answer action-specific questions about what the actor may do with a resource. A user may be allowed to view a project but not archive it, transfer it, delete it, or manage its members.

For that reason, larger systems often combine authorization-scoped resource loading with action-specific policies rather than treating either mechanism as a complete replacement for the other.


Actor, Action, Resource, and Environment

Authorization decisions frequently depend on more than the identity or role of the actor. In practice, the application may need to consider attributes of the actor, the requested action, the target resource, and the surrounding environment together. The actor may contribute information such as account state, role, organization membership, or entitlements, while the resource may contribute its owner, tenant, organization, current state, sensitivity, or visibility. Environmental conditions such as the current time or operational state may also affect the result.

This is why many real authorization systems extend beyond simple role-based access control. A role can be an important input, but it is often only one part of the decision. An administrator may receive broad access by virtue of role, while a normal member may need an active account, membership in the same organization as the resource, and sufficient permission for the specific action being performed.

The architectural objective is therefore to model the actual business decision rather than reduce every authorization rule to a role comparison. Authorization should reflect the combination of actor, action, resource, and relevant context that determines whether the operation is permitted.


Policy Names Should Describe Capabilities

Policy names should describe the capability being evaluated rather than expose only the individual conditions that contribute to the decision. Names such as canAccessAdmin, canViewProject, canEditProject, or canManageOrganizationMembers communicate the actual question the application is asking: whether this actor may perform a particular operation.

Lower-level predicates such as isAdmin, isOwner, isActive, or hasEntitlement can still be useful inside the policy layer, but they should not become the primary authorization interface. An isOwner check describes only one fact about the relationship between an actor and a resource. A policy such as canEditProject, by contrast, represents the complete authorization decision and can incorporate ownership together with account state, organization membership, role, resource state, or any other relevant condition.

This distinction reduces the amount of authorization knowledge required from callers. Rather than understanding which combination of predicates constitutes permission, the caller asks a single domain-level question and enforces the result.


Enforcement Depends on the Execution Context

Policies should determine whether an operation is allowed, but they should not decide how a denial is represented. Enforcement depends on the execution context. An HTTP endpoint may return 401 Unauthorized, 403 Forbidden, or 404 Not Found; a user-facing application may redirect to a login or access-denied page; a service may reject a command; and a background process may abort the operation and record an audit event.

Keeping these concerns separate prevents transport, framework, or runtime behavior from becoming embedded in the authorization rule itself. A policy such as canEditProject(actor, project) can therefore be reused across an HTTP endpoint, an RPC method, a background task, an administrative workflow, or a message-driven command.

Each of those callers may respond to denial differently, but the underlying authorization decision remains the same. The policy defines whether the action is permitted; the caller is responsible for enforcing that decision in the form appropriate to its environment.


404 Versus 403 for Object Access

For object-level resources, applications may choose to return 404 Not Found when the actor is not authorized to access the requested object. This avoids revealing whether a resource exists outside the actor’s authorization scope, which can be useful in systems where exposing the existence of another user’s or tenant’s resources would itself disclose sensitive information.

Authorization-scoped resource access supports this model naturally. If the application only searches within the set of resources visible to the current actor, then a failed lookup can mean either that the object does not exist or that it exists outside the actor’s permitted scope. The application does not need to distinguish between those cases in its external response.

This behavior is not appropriate for every system, but it is often useful for user-owned, tenant-owned, or otherwise sensitive resources where confirming the existence of an inaccessible object would provide unnecessary information.


Multi-Tenant Authorization

The same architectural separation becomes even more important in multi-tenant systems, where a resource belongs not only to an individual actor but also to an organizational boundary. A user from one organization should not be able to access a resource belonging to another organization merely by supplying a valid identifier.

For that reason, loading a resource only by its ID is often incomplete in a multi-tenant system. The tenant boundary must also be part of the authorization decision, either by scoping the resource lookup to the organizations the actor is allowed to access or by evaluating an explicit policy such as canViewProject(actor, project) after the resource has been identified.

The underlying invariant is the same as in other forms of object-level authorization: possession of a valid identifier does not imply access. Cross-tenant authorization failures are particularly serious because a single missing boundary check can expose data across entire organizations rather than between individual users. A centralized authorization model makes those tenant boundaries explicit and allows the same rules to be applied consistently wherever protected resources are accessed.


Deny by Default

Policies should generally follow a deny-by-default model, where access is granted only when an explicit rule allows it. An inactive or deleted account, for example, should be denied unless the policy contains a specific and intentional exception. Access may then be granted to an administrator or to an actor who satisfies the required entitlement, ownership, or tenant conditions.

The same principle applies to object-level authorization. A policy may deny inactive actors first, allow a defined administrative exception, and then evaluate whether the actor has the required relationship to the resource. If none of those conditions are satisfied, the result remains a denial.

This structure makes policy behavior easier to reason about because unexpected or unhandled states do not accidentally produce access. Permission must result from a known rule rather than from the absence of a denial condition.


Time as Authorization Context

Time-dependent access should be treated as an explicit input to the policy. Subscription and entitlement rules often depend on whether the current time falls within a defined validity interval, such as startAt <= now < endAt.

Passing the current time into the policy makes the decision deterministic and easier to test. It allows the system to verify behavior immediately before access begins, at the exact start time, just before expiration, at the expiration boundary, and after access has ended.

This keeps the policy focused on interpreting supplied facts rather than combining business logic with hidden runtime state. The caller provides the current time, and the policy evaluates whether the relevant entitlement or subscription is valid at that moment.


Testing Authorization as a Matrix

Authorization tests should evaluate combinations of actor, resource, and action rather than testing only successful access. For an object-level policy, this means covering both expected permissions and deliberate boundary violations: an owner accessing their own resource, a user attempting to access another user’s resource, a member crossing a tenant boundary, an administrator exercising elevated access, or a suspended or deleted actor attempting to use permissions they would otherwise hold.

This is particularly important for preventing IDOR vulnerabilities. A test suite should not only demonstrate that legitimate users can access valid resources; it should also demonstrate that unauthorized users cannot access valid resources belonging to someone else. Object-level authorization failures often pass ordinary functional tests because the resource exists, the operation succeeds, and the requester is authenticated. Only explicit cross-user and cross-tenant tests reveal whether the authorization boundary is actually being enforced.


Organizational Structure

The exact directory layout will depend on the application, language, and size of the codebase, but the ownership of each authorization concern should remain explicit. A small system may keep authentication, context loading, policies, and enforcement under a single authorization module, while a larger system may divide context and policy logic by domain, such as users, organizations, subscriptions, projects, or billing.

The specific names and folder structure are less important than the ability to locate each responsibility quickly. Developers should be able to determine where identity is established, where authorization facts are gathered, where access to a particular resource or capability is defined, and where denial is translated into transport or runtime-specific behavior.

If answering those questions requires searching through dozens of endpoints, services, and handlers, the authorization model is probably too distributed. A clear structure makes the system easier to review because the rules governing access are concentrated in known locations rather than embedded throughout unrelated application code.


Security Properties of the Architecture

This organization provides several important security and operational properties. Because authorization decisions are defined centrally, the same policy can be reused across HTTP handlers, services, background jobs, administrative tools, and other execution paths. Callers no longer need to reconstruct complex authorization expressions independently, which reduces duplication and the risk that different parts of the system will apply slightly different rules.

The model also strengthens object-level authorization by evaluating access in relation to both the actor and the target resource, rather than inferring permission from authentication or possession of a valid identifier. This makes the effective authorization model easier to audit, because reviewers can inspect the policy layer to understand how access is determined, and easier to test, because policies can be evaluated independently against combinations of actor, resource, action, and environmental context.

In multi-tenant systems, the same structure makes tenant boundaries more explicit. Membership and ownership rules can be enforced through authorization-scoped resource access, explicit object-level policies, or both. A deny-by-default approach further ensures that unexpected or unhandled conditions result in denial rather than accidental access.

These properties do not remove the need for secure implementation. Any execution path that bypasses the authorization layer can still introduce a vulnerability. The value of the architecture is that it establishes a clear and consistent path for protected operations and makes deviations from that path easier to identify during development and review.


Outcome

Authorization should be treated as a system of decisions rather than a collection of isolated checks. Authentication establishes identity, the authorization context gathers the facts required for a decision, the resource context identifies the object being acted upon, policies determine whether the actor may perform the requested action, and enforcement translates that decision into behavior appropriate to the current execution environment.

For object-level access, this structure directly addresses the conditions that produce IDOR vulnerabilities. The application no longer treats an authenticated actor with a valid object identifier as implicitly authorized. Instead, access is determined by evaluating the actor, the target resource, the requested action, and any other relevant context together.

Where appropriate, resource access can also be scoped to the actor’s authorized set so that inaccessible objects are not treated as valid candidates in the first place. Combined with action-specific policies, this creates an authorization model that is more explicit, more testable, and easier to review.

The objective is not to introduce a generic permission framework before the application needs one. It is to make authorization a defined part of the system architecture, with clear ownership of identity, context, policy, resource scope, and enforcement. By doing so, the application reduces the likelihood of inconsistent access checks and object-level authorization failures because permission is no longer inferred from session state, route access, or knowledge of a resource identifier. It is evaluated explicitly.