[ CAPABILITY / 03 ] · ENGINEERING INSIGHT
Authorization is a property of the whole request
A practical model for identity, tenant scope and permission checks that survive beyond the navigation menu.
[ 02 ]
Model scope alongside roles
Roles compress common permission sets, but a role alone rarely captures an enterprise hierarchy. A user may be an administrator in one organization and a read-only member of another; a branch manager may approve transactions only for assigned branches. Represent membership, role, action and resource scope separately.
Avoid creating a role for every combination of branches and permissions. Instead, define small actions such as invoice.read or inventory.adjust and evaluate them against server-derived membership. Keep exceptional rules explicit: an approval may require an amount threshold, separation of duties or a current workflow state.
Enforce filters in the database query as well as at the service boundary. Fetching a record by identifier and checking scope afterward can accidentally reveal existence through timing, error messages or a forgotten check. Return a consistent not-found or forbidden response according to the product’s disclosure policy.
[ 03 ]
Validate inputs before side effects
Validate shape, length and allowed values at the API boundary, then validate business rules inside the domain operation. A syntactically valid branch identifier may still refer to a branch outside the actor’s organization. Never accept role, price, tenant or ownership claims from a request body as authoritative merely because the frontend sent them.
For GraphQL, authorization belongs in the resolver or shared service handling each field and mutation, not only in a top-level gateway. Bound query complexity and pagination, avoid leaking sensitive errors, and ensure a cached result cannot be reused across tenants or permission contexts.
function canReadBranch(
membership: { organizationId: string; branchIds: readonly string[] },
resource: { organizationId: string; branchId: string },
): boolean {
return membership.organizationId === resource.organizationId
&& membership.branchIds.includes(resource.branchId);
}[ 04 ]
Treat sessions as revocable access
Short-lived access tokens reduce the lifetime of a stolen token but do not by themselves provide immediate revocation. Refresh-token rotation can detect reuse when token families are stored and invalidated correctly. A server-side session offers straightforward revocation at the cost of a session store and an additional lookup.
For browser applications, secure, HttpOnly cookies reduce direct JavaScript access to session material; SameSite policy and CSRF defenses still need to match the deployment. Validate token signatures, issuer, audience and expiry where applicable. Do not place credentials in logs, URLs, analytics events or persistent client storage without understanding the exposure.
Permission changes need a propagation strategy. Either check current membership for sensitive operations or invalidate affected sessions and caches. A token containing yesterday’s permissions should not indefinitely override today’s removal of access.
[ 05 ]
A policy is more than a role lookup
A permission decision combines trusted facts: the authenticated principal, active membership, requested action, resource ownership and current business state. For an approval, the policy may also require that the requester and approver differ. Keep these inputs visible in a policy interface so reviewers can understand why a decision was allowed or denied.
The data fetch and the mutation need to honor the same scope. When a record can change between the check and the write, perform the authorization-sensitive operation in a transaction or use a conditional update that includes the expected scope and state. Checking a draft and later updating an independently fetched record creates a window where the original decision may no longer be valid.
For denied access, use a consistent response policy. Some products intentionally return not found for resources outside the user’s scope to avoid confirming their existence. Internally, log enough context to investigate the denial, but avoid putting sensitive record contents into security logs.
[ 06 ]
Protect the browser boundary
Cookie-based sessions need an explicit CSRF strategy because browsers can attach cookies to requests automatically. SameSite settings are helpful but should be evaluated with the actual cross-site login and deployment flows; origin checks and anti-CSRF tokens may also be appropriate. Set Secure and HttpOnly where supported and restrict cookie scope to what the application needs.
Cross-site scripting can still perform actions as the user even when JavaScript cannot read an HttpOnly cookie. Escape untrusted output, avoid unsafe HTML insertion, review third-party scripts and adopt a content security policy that fits the application. Security headers help constrain a mistake; they do not make untrusted input safe on their own.
Account recovery and login are part of the same access system. Rate-limit sensitive endpoints, use generic responses where account enumeration is a concern, expire recovery tokens and invalidate them after use. Authentication dependencies should handle passwords and token verification through maintained, well-reviewed implementations rather than custom cryptography.
[ 07 ]
Jobs and integrations are principals too
A scheduled export or webhook processor may run without an interactive user, but it still needs a defined authority and tenant scope. A job payload containing an organization identifier is not automatically trustworthy; validate it against the job’s origin and permitted operation. If the task was initiated by a user, decide whether permission is checked at creation, execution or both.
Webhook signatures establish that a message was signed according to the provider’s scheme; they do not establish that it belongs to the correct order or has not already been processed. Verify the raw request as required by the provider, enforce replay and deduplication rules, and match the event to an internal reference before changing state.
Secrets must be provided through the deployment environment or a secret manager, not bundled into frontend code. Rotation requires a rollout plan: make the new credential available, confirm consumers use it, revoke the old credential and investigate failures. Access to logs and dashboards should follow the same least-privilege principle as the application itself.
[ 08 ]
Test the negative space
A useful authorization test starts with two organizations and at least two branches. Create resources in each, then exercise reads, mutations, exports and background tasks with actors whose memberships differ. Attempt direct identifiers, stale sessions and changed roles; assert both the response and the absence of an unauthorized side effect.
Tests should cover policy combinations that are easy to miss: an administrator in one organization who is a viewer in another, a user removed between queueing and execution, and a cached response requested after a permission change. The purpose is to test the boundary, not to prove that a menu hides a link.
Dependency scanning and penetration testing can expose important classes of problems, but neither proves business authorization correct. A review of the actual data flow, paired with a maintained regression matrix, is what keeps a product-specific rule visible as the application evolves.
Test revocation as a sequence, not a single response: create a session, access a permitted resource, remove membership, then attempt the same action through the API and any queued jobs. The expected behavior depends on the documented revocation window, but that window should be measurable and visible to the team.
Include failure behavior when the policy store or session store is unavailable. For operations that expose sensitive data or change state, returning an error is preferable to treating an unknown permission as granted. Distinguish an unavailable authorization service from a genuine denial in operational telemetry without exposing implementation details to the caller.
Review permission changes as product changes. A new export format, search index or administrative endpoint can create another route to an existing resource, even if it introduces no new role. Record the intended policy with the feature and add a negative test so a later refactor cannot accidentally broaden access.
[ 09 ]
Expect failures in the seams
Authorization errors often appear between features: an export uses a broader query than the screen, a background job lacks tenant context, or a cache key omits organization and user scope. Build a test matrix for anonymous, authorized and unauthorized users across tenants, branches, roles and direct object references.
Log security-relevant decisions with an actor identifier, action, resource category and correlation identifier, while excluding secrets and unnecessary personal data. Rate limiting, secure headers, dependency maintenance and operational alerting are complementary controls, not substitutes for authorization.
The most effective review question is specific: “Can this actor perform this action on this resource in this organization at this moment?” A generic authenticated middleware cannot answer that question by itself.
The permission check belongs to the operation, not to the button that happens to invoke it.
[ FIELD NOTES ]
Key takeaways
- Separate identity, action and resource scope.
- Validate both request shape and business rules.
- Plan session revocation and permission changes.
- Test exports, jobs and caches across tenant boundaries.
RELATED TECHNOLOGIES