All writing

Authentication and application security

Authorization Context Is Data: Designing Decisions That Survive Workflow Changes

Authorization becomes dependable when each decision is explicit about its actor, resource, action, relationships, and current workflow state.

Mubashir Hussain10 min read

An authorization bug is rarely caused by a missing if alone. More often, the application made a decision with stale, incomplete, or ambiguous information. A signed-in user can be the wrong actor for a resource, can be entitled only in one organization, or can arrive after the workflow state has changed. The user interface may hide the button, a route guard may recognize a session, and a token may be valid, yet the operation can still be unsafe.

That is why authorization deserves to be treated as a decision made from data, rather than as a convenience check bolted onto endpoints. A durable design names the actor, action, resource, relationship, tenant, and current state that matter; evaluates them at the server-side boundary; and leaves enough evidence to test and investigate the result. The point is not to build a universal policy engine. It is to make the application’s real policy visible and difficult to accidentally bypass.

Authorization begins after authentication

Authentication answers who presented a credential. Authorization answers whether that principal may perform this specific action on this specific resource under current conditions. Conflating the two turns a valid session into an accidental blanket permission. A user who can sign in to a marketplace, for example, is not automatically allowed to release a payment, view another participant’s evidence, or alter a completed handoff.

OWASP’s authorization guidance emphasizes least privilege, denial by default, and checking permissions on every request. Those recommendations are practical design constraints. They mean that a handler should not infer permission from a menu item, an HTTP path, a role label, or a client-provided identifier. Each may be a useful input, but none is the decision by itself.

Start by writing permissions as complete sentences: “A verified courier may confirm receipt for a package assigned to them while the delivery is in the handoff state.” That statement exposes the inputs a code path needs: the authenticated courier, the package, the assignment relationship, and the delivery state. If an endpoint cannot gather and verify those inputs, it is not ready to make the decision.

Make the decision contract explicit

A useful authorization contract has five parts: actor, action, resource, context, and outcome. The actor is the server-derived principal. The action is a stable verb such as booking.cancel, invoice.read, or handoff.confirm. The resource is the loaded domain record, not only an ID from a URL. Context contains the attributes that alter policy: tenant, relationship, verification status, time window, feature entitlement, or workflow state. The outcome is an allow or deny plus a narrow reason code suitable for operations.

This structure is small enough to use inside ordinary application services. A route can authenticate, parse input, load the resource through the correct tenant scope, construct the context, and ask a policy function whether the action is allowed. The mutation should occur only after that function returns allow, ideally in the same transaction or under the same concurrency control that protects the state transition.

The contract also prevents a common drift: one handler checks ownership, another checks a broad role, and a background job skips both because it is “internal.” If the job initiates the same business action, it should use the same decision vocabulary. Its actor may be a service principal rather than a person, but it still needs a deliberately granted capability and an auditable reason for acting.

Treat current state as an authorization input

Many permissions are temporary. A buyer can cancel before a handoff, a reviewer can approve while an item is pending, and a support operator can intervene only before final settlement. Modeling those rules only in client flow logic creates a race: a user can retain an old screen or resend a captured request after the state has moved on.

Instead, load the authoritative state at the decision point and make the permitted states part of policy. A transition should declare both its source states and its required authority. In a simple form, the update can require the expected current state as a condition, then report whether it changed exactly one record. In a richer system, a state-machine service can validate the transition and authorization together before committing the event.

This approach improves correctness beyond security. It explains why a request was rejected: the actor might be valid but the action is no longer meaningful. It also makes retries safer, because a repeated command encounters an already-transitioned state instead of silently applying a second side effect. Authorization and concurrency are different concerns, but workflow operations need both to preserve the same business invariant.

Keep token claims in their proper role

Access tokens are valuable evidence about an authenticated client and its delegated authority, but they are not a copy of all present-day business truth. Memberships can be revoked, assignments can change, accounts can be suspended, and a resource can move to another organization after a token is issued. Treating a role claim as a permanent answer to every resource-level question invites stale authorization.

RFC 9068 defines a profile for OAuth 2.0 JWT access tokens, including claims that resource servers validate when accepting tokens. RFC 8725 describes security practices for JWT use, including validating intended recipients and constraining how tokens are interpreted. Those standards support a disciplined boundary: validate the token as a token, then use server-side records for relationships and mutable business attributes.

That does not require a database query for every harmless page decoration. It does mean that a sensitive mutation should be authorized against data whose freshness matches the risk. A cached entitlement may be appropriate for low-risk navigation when its invalidation is well understood; an irreversible payout or evidence download normally merits a current, resource-specific check. Record that freshness choice in the policy rather than letting it emerge from cache behavior.

Use roles for coarse grants and relationships for real work

Roles remain useful for broad capabilities: an administrator can enter an operations area; an accountant can initiate a reconciliation process. The difficulty begins when a role tries to encode every resource-specific exception. “Editor for organization A,” “editor for organization A during review,” and “editor for organization A except archived reports” soon become a role explosion that is hard to review.

OWASP recommends considering attribute- and relationship-based access control when the domain needs more precision than a role alone can express. A relationship may say that a user owns a listing, is assigned to a booking, is a member of the current organization, or is the delegated approver for an invoice. Attributes can add the resource classification, account status, assurance level, or local workflow phase.

The implementation can stay straightforward. Store relationships in domain tables or collections with explicit tenant boundaries. Load only the relationship relevant to the requested action. Then make a small, readable policy expression that joins the actor, resource, and relation. A query that scopes a record by organization and assignment is often safer than fetching by ID and hoping a later condition catches an isolation mistake.

Put enforcement where bypass is difficult

Client-side checks are excellent for preventing confusing interfaces, but they cannot be the authority. Users can call a route directly, modify a request, or invoke a background-facing endpoint in an unexpected order. OWASP explicitly advises placing decisive authorization on the server side, gateway, or equivalent enforcement boundary.

For an application with server-rendered routes and APIs, this often produces layers with different jobs. Middleware may block unauthenticated access to a private area. A page or route can establish session context. The service handling a mutation performs the resource-specific decision. Storage access adds tenant predicates and transaction conditions. These layers are complementary; replacing the final decision with an earlier navigation guard leaves a gap.

NIST’s zero trust architecture frames access decisions around dynamic evaluation rather than implicit trust based on network location. Applied to product code, the useful lesson is modest: internal origin, familiar device, or prior navigation should not erase the need to evaluate the request and resource at the point where a sensitive operation happens. The scope of evaluation should be proportional to the action, not to where the request appeared to come from.

Design denials as normal outcomes

A secure application encounters denied requests in ordinary use: someone follows an old link, loses an assignment, switches organizations, or tries an action after a record changes state. Treating every denial as an exceptional server failure makes user recovery and debugging worse. Treating every failure as “not found” can also make support impossible when a legitimate user needs to understand a changed permission.

Define a small set of non-sensitive outcome codes, such as not_authenticated, not_member, relationship_required, state_not_permitted, and action_not_allowed. Map them to carefully chosen HTTP responses and user messages without revealing whether an unrelated resource exists. The audit record can carry more context for authorized operators: actor ID, action, resource type and ID, organization, policy version, outcome, and correlation ID. It should not indiscriminately log credentials or private evidence.

Clear denials also make product trade-offs visible. If a user can no longer take an action because a task was reassigned, the product may offer a safe next step rather than merely displaying an error. The authorization decision remains strict while the surrounding experience becomes understandable.

Test permissions as a behavior matrix

Authorization tests should not be a single happy-path test for each route. Build a matrix across actors, resources, relationships, states, and tenants. For a booking operation, cover the assigned provider, an unassigned provider, the requester, an administrator, a user from another organization, and an anonymous request. Then repeat the cases in each meaningful workflow state.

Tests should call the same service or endpoint used in production. Unit tests are valuable for policy functions, while integration tests catch missing tenant filters, serialization differences, and transaction sequencing. Add regression cases whenever an incident exposes a missed boundary. A table-driven test suite makes omissions easier to spot because a new action must declare its rows instead of relying on an implicit convention.

Operational review matters too. Look for unusually frequent denials, attempts against resources outside a user’s organization, and state-transition failures after deployments. Those signals can reveal abuse, but they can also show that a policy no longer matches the product’s intended workflow. Policies should be reviewed as requirements change, not frozen as one-time security plumbing.

Apply the pattern to multi-party products

The portfolio examples make the shape of the problem concrete without changing its general rule. Be My Courier includes courier-marketplace workflows involving package safety, traveler evidence, order state, payment authority, and staff operations. Mining Access includes listings, negotiations, delivery, approvals, reviews, payouts, and moderation. CEPRA includes organization-specific data and operational workflows. In each kind of system, a coarse “logged-in user” check cannot communicate who may execute the next business transition.

A practical implementation would attach authorization to the domain service that already understands the transition. A payout action could require the service actor, the right organization, the relevant settlement state, and a narrowly granted operations relationship. A moderation action could require an operator capability plus the policy that permits access to the affected organization’s record. The important point is not a particular framework; it is that each decision is made from authoritative, named inputs.

This also preserves contribution accuracy. The portfolio facts describe explicit state coordination, role-aware or organization-aware systems, and integration-heavy operations. They do not justify claims about a single universal authorization solution or measured security outcomes. Good technical writing should make the architectural lesson clear without extending the evidence beyond what is known.

Build for change, not for a perfect initial policy

Authorization will change because products gain roles, delegated access, new workflow states, external providers, and operational exceptions. The goal is not to predict every future rule. The goal is to keep policy inputs, enforcement points, and tests sufficiently explicit that a new rule can be added without creating a hidden bypass.

Begin with a written action inventory, deny by default, and one server-side policy boundary per sensitive operation. Add resource relationships and current state where the domain needs them. Validate tokens according to their intended use, but obtain mutable business truth from authoritative records. Finally, test the matrix of actors and states whenever a workflow changes.

When authorization context is treated as data, access control stops being a scattered collection of checks. It becomes a maintainable part of the product model: specific enough to protect real workflows, visible enough to review, and adaptable enough to survive the next change.

Primary sources

  1. 1.Authorization Cheat Sheet — OWASP Foundation
  2. 2.SP 800-207: Zero Trust Architecture — National Institute of Standards and Technology
  3. 3.RFC 9068: JSON Web Token Profile for OAuth 2.0 Access Tokens — IETF
  4. 4.RFC 8725: JSON Web Token Best Current Practices — IETF

Portfolio evidence

Related writing