Build CoreOrdered learning track

Authorization Mental Model

Learn React Authentication, Authorization, Identity & Permission/ACL - Part 041

Authorization mental model for React engineers: subject-action-resource-context, policy decision point, policy enforcement point, deny-by-default, decision contracts, permission projection, workflow authorization, tenant boundaries, cache invalidation, and testing strategy.

14 min read2787 words
PrevNext
Lesson 41130 lesson track25–71 Build Core
#react#authorization#access-control#permission+7 more

Part 041 — Authorization Mental Model

Authentication answers:

Who is interacting with the system?

Authorization answers:

Given this authenticated or anonymous subject,
can this subject perform this action
on this resource
under this context
right now?

That extra right now matters.

Authorization is not a static label attached to a user.

Authorization is a decision.

And like every important decision in a production system, it needs:

  • inputs,
  • policy,
  • a decision point,
  • an enforcement point,
  • an audit trail,
  • denial semantics,
  • test cases,
  • operational visibility,
  • failure behavior.

React is not the final enforcement boundary.

But React still needs a precise authorization model because it decides which surfaces are shown, which actions are enabled, which data loaders run, which error states appear, and how safely the product behaves when authorization changes while the user is already inside the app.

The mistake is not “doing authorization in React”.

The mistake is believing React authorization is enough.


1. The minimum useful model

The minimum authorization question is:

Can subject S perform action A on resource R in context C?

In code, that becomes:

can(subject, action, resource, context) => decision

Example:

can(
  { type: "user", id: "u_123", tenantId: "t_001" },
  "case.approve",
  { type: "case", id: "case_900", tenantId: "t_001", status: "UNDER_REVIEW" },
  { ipRisk: "low", assuranceLevel: 2, now: "2026-07-08T10:00:00+07:00" }
)

A weak app asks:

Is this user an admin?

A serious app asks:

Can this subject perform this action on this exact resource under this exact situation?

That is the difference between role display logic and authorization engineering.


2. Authorization is not one thing

Authorization appears in multiple shapes:

ShapeQuestionExample
Route authorizationCan the user enter this route?Can open /admin/users?
Data authorizationCan the user read this dataset?Can list enforcement cases?
Object authorizationCan the user access this object?Can open case C-123?
Action authorizationCan the user do this operation?Can approve, reject, assign?
Field authorizationCan the user see or edit this field?Can edit penalty amount?
Workflow authorizationCan the user perform this transition now?Can move DRAFT → APPROVED?
Tenant authorizationIs the subject allowed in this tenant/org/project?Can act in tenant acme?
Delegated authorizationIs access granted through relationship?Owner, manager, reviewer, assignee.
Step-up authorizationIs current authentication assurance sufficient?Require MFA before payout.
Administrative authorizationCan the user change authorization itself?Can grant admin role?

A React app often touches all of them in the same screen.

A case detail page may need:

route: user may open case detail route
read: user may read this specific case
action: user may approve this case
field: user may see confidential notes
workflow: case may transition from UNDER_REVIEW to APPROVED
tenant: user and case belong to same tenant
step-up: approval over threshold requires MFA

If your frontend auth model only has isAdmin, it cannot represent the real system.


3. The authorization pipeline

A mature authorization system has four conceptual parts.

The names are less important than the separation.

ConceptMeaningIn a React system
PAPWhere policy is authored/administeredAdmin console, role editor, grant workflow.
PIPWhere attributes/relationships come fromUser profile, org membership, resource metadata, workflow state.
PDPComponent that evaluates policyBackend service, policy engine, API layer, BFF.
PEPComponent that enforces decisionAPI handler, route loader, action handler, middleware, backend service.

React can consume decisions.

React can request decisions.

React can project decisions into UI.

React should not be the only PEP for real resources.


4. Frontend authorization is projection, not authority

Frontend authorization has a legitimate job:

Expose the product safely based on known decisions.

It should:

  • hide irrelevant navigation,
  • disable impossible actions,
  • show permission reasons,
  • route unauthenticated users to login,
  • route authenticated-but-forbidden users to denial pages,
  • prevent accidental invalid submissions,
  • reduce support noise,
  • keep the UI coherent while authorization is loading,
  • avoid leaking protected content before decision is known.

It must not:

  • be the only thing stopping access,
  • infer object access from route params,
  • trust decoded JWT claims as final permission,
  • let hidden buttons define policy,
  • treat roles as universal permissions,
  • cache permission forever,
  • allow frontend-only workflow transitions,
  • skip server-side checks because a component already checked.

A useful rule:

React decides what the user sees.
The server decides what the user can do.

That rule is not perfect, because SSR/BFF loaders blur the line.

But the invariant remains:

Every resource read and write must pass an enforcement boundary outside the user's control.

5. Subject, action, resource, context

5.1 Subject

The subject is the actor.

Usually a user, but not always.

Examples:

type Subject =
  | { type: "anonymous" }
  | { type: "user"; id: string; tenantId: string; roles: string[] }
  | { type: "service"; id: string; scopes: string[] }
  | { type: "impersonated-user"; actorId: string; effectiveUserId: string; tenantId: string };

Subject is not equal to profile.

Profile data answers:

What is this user's display name, email, avatar, locale?

Authorization subject answers:

What principal is acting, under which tenant, with which authority and assurance?

Avoid mixing them.

A profile object is often too noisy and too stale for authorization.

5.2 Action

An action is the operation being requested.

Good actions are domain verbs:

case.read
case.create
case.assign
case.approve
case.reopen
case.close
case.comment.create
case.evidence.download
case.penalty.edit
user.invite
role.grant

Weak actions are UI verbs:

button.click
modal.open
tab.view
page.visit

UI verbs are useful for analytics.

They are not stable policy semantics.

A strong action vocabulary should be:

  • domain-oriented,
  • stable across UI redesigns,
  • granular enough for real policy,
  • not so granular that it creates unmanageable noise,
  • shared between frontend, backend, tests, and documentation.

5.3 Resource

The resource is the target.

It may be:

a tenant,
a project,
a case,
a document,
a field,
a workflow transition,
a report,
a saved search,
a user account,
a role assignment,
a policy object.

A resource is not just an ID.

A resource often carries attributes relevant to authorization:

type CaseResource = {
  type: "case";
  id: string;
  tenantId: string;
  status: "DRAFT" | "UNDER_REVIEW" | "APPROVED" | "CLOSED";
  ownerUserId: string;
  assignedTeamId: string;
  confidentiality: "normal" | "restricted" | "sealed";
  penaltyAmount?: number;
};

If React only knows caseId, it cannot evaluate meaningful object/action permission locally.

That is fine.

React can request a decision from the server.

5.4 Context

Context is everything that matters but is not the subject/action/resource itself.

Examples:

type AuthorizationContext = {
  now: string;
  tenantId: string;
  requestId: string;
  assuranceLevel: 1 | 2 | 3;
  ipRisk?: "low" | "medium" | "high";
  deviceTrust?: "unknown" | "managed" | "trusted";
  sessionAgeSeconds?: number;
  reason?: "user_action" | "background_poll" | "preload";
};

Context prevents authorization from becoming a misleading static lookup.

Examples:

User can export data only during business hours.
User can approve payout only after MFA.
User can edit case only while status is DRAFT.
User can download evidence only from managed device.
User can invite users only inside current tenant.

React should not invent high-trust context.

But React may pass low-trust context as input if the server validates it.


6. Decision shape

A boolean is usually too small.

Bad decision contract:

type Decision = boolean;

Better decision contract:

type AuthorizationDecision =
  | {
      effect: "allow";
      decisionId: string;
      obligations?: AuthorizationObligation[];
      ttlSeconds?: number;
    }
  | {
      effect: "deny";
      decisionId: string;
      reason:
        | "not_authenticated"
        | "insufficient_permission"
        | "tenant_mismatch"
        | "resource_not_found_or_hidden"
        | "workflow_state_denied"
        | "step_up_required"
        | "policy_unavailable";
      messageKey: string;
      retry?: "login" | "step_up" | "request_access" | "never";
    };

type AuthorizationObligation =
  | { type: "mask_field"; field: string }
  | { type: "watermark_export" }
  | { type: "audit_reason_required" }
  | { type: "max_export_rows"; value: number };

Why richer decisions matter:

  • React can render denial correctly.
  • UX can distinguish login from forbidden from step-up.
  • Audit logs can correlate UI and backend decisions.
  • Tests can assert the reason, not just the boolean.
  • Product can show request-access flows.
  • Policies can add obligations without rewriting every screen.

But be careful.

Denial reasons can leak information.

For sensitive objects, the backend may intentionally return:

resource_not_found_or_hidden

instead of:

forbidden_because_case_is_sealed

The UI should support this without trying to be clever.


7. Deny-by-default is a runtime behavior

Deny-by-default is not a slogan.

It means:

When permission state is unknown, do not expose protected capability.
When policy service fails, do not allow sensitive action by default.
When resource context is missing, do not guess.
When tenant is ambiguous, do not proceed.
When a route has no policy metadata, do not assume public unless explicitly marked public.

In React, deny-by-default shows up as:

function Can({ decision, children, fallback = null }: Props) {
  if (decision.status !== "ready") return fallback;
  if (decision.value.effect !== "allow") return fallback;
  return <>{children}</>;
}

The insecure version is:

function Can({ loading, allowed, children }: Props) {
  if (loading) return <>{children}</>; // unsafe flash
  if (!allowed) return null;
  return <>{children}</>;
}

Unknown is not allowed.

Loading is not allowed.

Missing data is not allowed.

Policy failure is not allowed for sensitive actions.


8. Authorization is per request, not per page load

A common frontend mistake:

The page loaded, so all actions on the page are allowed.

That is false.

A page may be readable while some actions are forbidden.

Example:

case.read: allow
case.comment.create: allow
case.assign: deny
case.approve: deny because status is DRAFT
case.evidence.download: deny because confidentiality is restricted

Another mistake:

The user loaded the app with Manager role, so Manager permissions remain valid until logout.

Also false.

Permissions can change while the session is active.

Authorization is a moving decision.

Triggers that can invalidate permission:

  • role revoked,
  • user removed from tenant,
  • resource status changed,
  • object ownership changed,
  • case assigned to another team,
  • document sealed,
  • step-up expired,
  • session risk changed,
  • policy deploy changed,
  • feature moved from preview to restricted release,
  • user is impersonating another subject.

React needs a permission invalidation story.

Not every decision needs to be live-updated every second.

But every sensitive write must be rechecked at enforcement time.


9. Route authorization vs resource authorization

Route authorization is coarse.

Resource authorization is precise.

A user may be allowed to open the cases route but not allowed to see every case.

In serious systems:

route access is necessary but not sufficient.

React Router loaders help because they allow route-level decisions before render.

But the loader still needs backend support for actual resource access.

A loader that fetches everything and filters in the browser is not secure.


10. Action authorization and workflow state

For write operations, the authorization question must include the operation and resource state.

Bad mutation endpoint:

POST /cases/123/approve
Authorization: Bearer token

Backend check:

user.role == "manager"

That is not enough.

Better check:

subject is authenticated
subject belongs to tenant of case
subject can perform case.approve on this case
case.status == UNDER_REVIEW
subject is not the same user who created the case if separation-of-duty applies
current assurance level satisfies approval threshold
policy version is active
operation is idempotent or replay-safe

React's job:

  • do not show approve button when obviously denied,
  • explain why action is unavailable when safe,
  • require step-up before submitting if policy says so,
  • handle backend denial even if UI thought it was allowed,
  • rollback optimistic UI,
  • invalidate resource data after mutation,
  • surface correlation ID for support.

Backend's job:

  • enforce the real policy,
  • validate workflow state,
  • write audit event,
  • handle race conditions transactionally.

11. Authorization granularity

Too coarse:

admin
user
viewer

Too fine:

case-detail-screen-header-left-export-button-hover-tooltip-open

Useful granularity:

case.read
case.create
case.update
case.assign
case.approve
case.close
case.comment.create
case.evidence.upload
case.evidence.download
case.penalty.edit
case.audit.read

A practical rule:

Model permissions around domain operations and data boundaries, not component names.

Ask:

  • Would this action still exist if the UI changed?
  • Does this action map to a backend operation?
  • Does this action require separate audit?
  • Could this action have different permission from similar actions?
  • Does this action expose sensitive data?
  • Does this action alter workflow state?

If yes, it probably deserves a permission/action.


12. Permission is not role

Role is a grouping mechanism.

Permission is a capability.

Decision is the result of evaluating subject/action/resource/context against policy.

role -> grants permissions
permission -> describes possible capability
decision -> says whether capability applies now

Example:

Role: Case Manager
Permission: case.approve
Decision: deny because case status is DRAFT

This distinction prevents a common bug:

if (user.roles.includes("case_manager")) {
  return <ApproveButton />;
}

The user may be a case manager but still cannot approve this case.

The better question:

const decision = useCan("case.approve", caseResource);

13. Scope is not app permission

OAuth scopes and app permissions overlap but are not identical.

An OAuth access token scope may say:

cases:write

That may only mean:

This client/token is allowed to call write APIs for cases in principle.

It does not necessarily mean:

This user can approve this exact case.

Scopes often describe API-level delegation.

Domain permissions describe business authorization.

A production system often needs both:

Token scope allows the API category.
Domain policy allows the exact operation.

Backend check:

access_token.aud == case-api
token.scope contains cases:write
subject can case.approve resource case_123 under context C

React should not turn OAuth scopes into product policy without explicit design.


14. Role-based, attribute-based, relationship-based, ACL

Different authorization models answer the same question using different inputs.

ModelMain inputGood forWeakness
RBACRoles assigned to usersStable org/job functionsRole explosion, weak object-level nuance
ABACAttributes of subject/resource/contextContext-rich decisionsPolicy complexity, attribute freshness
ReBACRelationships between subjects/resourcesCollaboration, hierarchy, sharingRequires relationship graph discipline
ACLExplicit grants on objectPer-object sharingHard to manage at scale if uncontrolled

Most real systems are hybrid.

Example:

RBAC: User is Case Manager.
ABAC: Case is in same region and amount below threshold.
ReBAC: User is assigned reviewer of this case.
ACL: User was explicitly granted read access to sealed evidence.
Workflow: Case is UNDER_REVIEW.
Step-up: MFA completed within last 10 minutes.

React should not care which model produced the decision.

React should care about the decision contract.

type PermissionProjection = {
  resourceId: string;
  allowedActions: string[];
  denied?: Record<string, { reason: string }>;
};

The policy engine can evolve behind this contract.


15. Permission projection

React rarely needs raw policy.

React usually needs projection.

Projection means:

A backend-owned representation of what this subject can see/do in this UI context.

Example response:

{
  "case": {
    "id": "case_123",
    "status": "UNDER_REVIEW",
    "title": "Enforcement review",
    "confidentiality": "normal"
  },
  "permissions": {
    "case.read": { "effect": "allow" },
    "case.comment.create": { "effect": "allow" },
    "case.assign": {
      "effect": "deny",
      "reason": "insufficient_permission"
    },
    "case.approve": {
      "effect": "deny",
      "reason": "step_up_required",
      "retry": "step_up"
    }
  }
}

Benefits:

  • avoids hardcoding policy in components,
  • keeps UI coherent,
  • supports per-resource decisions,
  • gives denial reason,
  • reduces duplicate decision calls,
  • makes tests deterministic,
  • allows backend policy evolution.

But projection has risks:

  • it can become stale,
  • it may overexpose denied action reasons,
  • it may be too large,
  • it may tempt teams to skip enforcement on mutation,
  • it may need cache scoping by tenant/resource/user/session.

Projection is a UI input.

It is not an enforcement proof.


16. Decision freshness

Authorization data has a freshness requirement.

Not all decisions need the same TTL.

Decision typeExampleFreshness expectation
Navigation exposureCan see Admin menuCan tolerate short cache
Read accessCan open this caseShould be checked at load time
Sensitive writeCan approve payoutMust be checked at submit time
Privilege adminCan grant roleMust be checked immediately
Step-upMFA freshnessUsually short TTL
Tenant membershipCan act in tenantShould invalidate quickly on removal

A useful frontend shape:

type DecisionEnvelope = {
  decision: AuthorizationDecision;
  evaluatedAt: string;
  policyVersion: string;
  subjectVersion: string;
  resourceVersion?: string;
  ttlSeconds?: number;
};

On role or tenant changes, invalidate permission queries.

On logout, purge all user/tenant-scoped permission state.

On mutation denial, do not keep trusting stale projection.


17. Tenant boundary is authorization input

In multi-tenant apps, tenant is not a theme setting.

Tenant is a security boundary.

Every authorization request should be tenant-aware.

Bad:

can(user, "case.read", { id: caseId }, {})

Better:

can(
  { id: userId, activeTenantId },
  "case.read",
  { type: "case", id: caseId, tenantId: caseTenantId },
  { tenantId: activeTenantId }
)

Tenant bugs often come from mismatch between:

  • active tenant in UI,
  • tenant in session,
  • tenant in URL,
  • tenant in resource,
  • tenant in cache key,
  • tenant in token claim,
  • tenant in API path/header,
  • tenant in policy decision.

React must scope caches by tenant.

queryKey: ["tenant", tenantId, "case", caseId]

Not:

queryKey: ["case", caseId]

Tenant switch must purge or invalidate:

  • resource queries,
  • permission queries,
  • route loader data,
  • form draft state if tenant-scoped,
  • open modals/actions,
  • WebSocket subscriptions,
  • command palette entries.

18. Authorization errors are product states

Auth errors are not generic failures.

Different causes need different recovery:

CauseUI response
Not authenticatedLogin, preserve safe return intent
Session expiredRe-authenticate, retry safe operation if possible
ForbiddenExplain lack of access or show generic denial
Step-up requiredStart MFA/reauth flow
Tenant mismatchSwitch tenant or show no-access
Resource hiddenShow not found or limited no-access state
Workflow deniedExplain current state if safe
Policy service downFail closed for sensitive operations; degrade non-sensitive UI

Do not reduce all failures to:

Something went wrong.

But do not reveal sensitive policy internals either.

Good denial copy is boring and precise:

You do not have access to approve this case.

Better when safe:

This case can only be approved after review is complete.

Unsafe:

You cannot approve this case because the sealed-evidence flag is active and only the internal anti-fraud team can access it.

19. Authorization as a state machine

A decision can move through states.

React components should not assume permission is permanently stable.

A robust component handles:

unknown
loading
allowed
denied
error
stale

Example:

function ApproveCaseAction({ caseId }: { caseId: string }) {
  const decision = useCan("case.approve", { type: "case", id: caseId });

  if (decision.status === "loading" || decision.status === "unknown") {
    return null;
  }

  if (decision.status === "denied") {
    if (decision.reason === "step_up_required") {
      return <StepUpButton intent="case.approve" resourceId={caseId} />;
    }
    return <DisabledAction reason={decision.reason} />;
  }

  if (decision.status === "error") {
    return <DisabledAction reason="permission_unavailable" />;
  }

  return <ApproveButton caseId={caseId} />;
}

Again:

This improves UX.
It does not replace backend authorization.

20. Enforcement placement

Enforcement must exist where data or state changes happen.

LayerEnforces?Why
React componentNo, only exposureUser controls browser runtime
React route loader in browserPartial exposureStill user-controlled if purely client-side
SSR/BFF loaderYes for data it returnsRuns server-side and can withhold data
API handlerYesOwns resource read/write boundary
Domain serviceYesProtects business operation across callers
Database row policySometimesStrong defense-in-depth for data access
Storage service/signed URLYesProtects binary object access
Message consumer/jobYesProtects async operations

The same operation may need multiple enforcement points.

Example file download:

React hides Download button.
API checks evidence.download.
API creates short-lived signed URL only if allowed.
Object storage enforces signed URL expiry.
Audit records download intent and issuance.

21. Authorization and caching

Caching authorization is dangerous when the cache key is incomplete.

Bad:

["permissions"]

Better:

[
  "permissions",
  subjectId,
  activeTenantId,
  resourceType,
  resourceId,
  policyVersion,
  subjectVersion
]

Cache invalidation triggers:

  • login,
  • logout,
  • token refresh with changed claims,
  • role assignment changed,
  • tenant switch,
  • resource status changed,
  • step-up completed/expired,
  • impersonation started/exited,
  • policy version changed,
  • backend returns 403 after UI allowed,
  • account disabled.

A mature system exposes versions:

{
  "subjectVersion": "user-memberships-v42",
  "policyVersion": "policy-2026-07-08.3",
  "tenantVersion": "tenant-acme-memberships-v19"
}

React can use these versions to invalidate authorization projections without hardcoding policy internals.


22. Authorization and audit

Authorization decisions should be auditable, especially denials and sensitive allows.

Audit event example:

{
  "eventType": "authorization.decision",
  "decisionId": "dec_01H...",
  "effect": "deny",
  "reason": "workflow_state_denied",
  "subjectId": "u_123",
  "tenantId": "t_001",
  "action": "case.approve",
  "resourceType": "case",
  "resourceId": "case_900",
  "resourceVersion": "7",
  "policyVersion": "policy-2026-07-08.3",
  "requestId": "req_abc",
  "uiRoute": "/cases/case_900",
  "timestamp": "2026-07-08T10:00:00+07:00"
}

React should not log secrets or full token values.

But React can help observability by passing:

  • request ID,
  • route ID,
  • action intent,
  • UI component source,
  • client build version,
  • active tenant ID,
  • correlation ID from backend error.

This makes production debugging possible without leaking credentials.


23. Test the model, not just the component

Authorization tests should cover the matrix.

Example matrix:

SubjectResourceActionContextExpected
anonymouscasereadnonedeny login
viewercase same tenantreadnormalallow
viewercase same tenantapprovenormaldeny
managercase same tenant draftapprovenormaldeny workflow
managercase same tenant reviewapproveMFA freshallow
managercase other tenantreadnormaldeny tenant
admin impersonating viewerrole grantnormaldeny or constrained

Do not only test happy paths.

Most authorization bugs live in negative cases.

Test categories:

anonymous
wrong tenant
wrong role
wrong resource state
wrong owner
revoked membership
stale permission cache
step-up expired
policy service unavailable
direct URL access
API call bypassing UI
optimistic UI rollback

24. Common anti-patterns

Anti-pattern 1: Role check in component

{user.role === "admin" && <DeleteUserButton />}

Problem:

Role is not action/resource/context decision.

Better:

<Can action="user.delete" resource={targetUser}>
  <DeleteUserButton />
</Can>

Anti-pattern 2: JWT claim as final permission

if (decodedToken.permissions.includes("case.approve")) {
  approveCase(caseId);
}

Problem:

Claim may be stale, broad, or unrelated to object/workflow state.

Better:

Use claims as hints only.
Server still authorizes the mutation.

Anti-pattern 3: Hidden button as enforcement

No approve button means no one can approve.

Problem:

User can call API directly.

Better:

UI exposure + API/domain enforcement + audit.

Anti-pattern 4: Global permission cache

const permissions = await fetch("/me/permissions");

Problem:

Global permissions cannot answer object-specific questions.

Better:

Use global projection for navigation.
Use resource-specific projection for object/action decisions.

Anti-pattern 5: Treating 403 as logout

Problem:

Forbidden is not unauthenticated.

Better:

401 -> authenticate.
403 -> deny/request access/tenant switch/step-up if indicated.

25. Reference TypeScript model

type Effect = "allow" | "deny";

type SubjectRef = {
  type: "user" | "service" | "anonymous" | "impersonated-user";
  id?: string;
  tenantId?: string;
  assuranceLevel?: 0 | 1 | 2 | 3;
};

type ResourceRef = {
  type: string;
  id?: string;
  tenantId?: string;
  attributes?: Record<string, unknown>;
};

type AuthzRequest = {
  subject: SubjectRef;
  action: string;
  resource: ResourceRef;
  context: {
    tenantId?: string;
    routeId?: string;
    requestId?: string;
    now?: string;
  };
};

type AuthzDecision =
  | {
      effect: "allow";
      decisionId: string;
      obligations?: Array<{ type: string; [key: string]: unknown }>;
      ttlSeconds?: number;
    }
  | {
      effect: "deny";
      decisionId: string;
      reason:
        | "not_authenticated"
        | "insufficient_permission"
        | "tenant_mismatch"
        | "resource_not_found_or_hidden"
        | "workflow_state_denied"
        | "step_up_required"
        | "policy_unavailable";
      retry?: "login" | "step_up" | "request_access" | "never";
      messageKey: string;
    };

This model is intentionally boring.

Boring authorization contracts are good.

They are easy to test.


26. Production checklist

Before shipping an authorization-sensitive React feature, ask:

[ ] What is the subject?
[ ] What is the action name?
[ ] What is the resource?
[ ] What context matters?
[ ] Which backend boundary enforces it?
[ ] What does React use for exposure control?
[ ] What happens while permission is unknown?
[ ] What happens if the decision is stale?
[ ] What happens if backend denies after UI allowed?
[ ] Is tenant included in cache keys?
[ ] Are denial reasons safe to show?
[ ] Is there a step-up path if needed?
[ ] Is the decision auditable?
[ ] Are negative cases tested?
[ ] Is permission invalidated on role/tenant/session changes?

If the answer is “the button is hidden”, the design is incomplete.


27. The mental compression

Keep this compact model in your head:

Authentication gives you a subject.
Authorization evaluates subject + action + resource + context.
Policy produces a decision.
React projects the decision into UI.
The server enforces the decision on data and mutation boundaries.
Every sensitive operation is deny-by-default, per-request, auditable, and tested.

That is the foundation for RBAC, ABAC, ReBAC, ACL, permission-aware components, route policy metadata, and state-based workflow authorization.

The next part zooms into RBAC.

But RBAC only makes sense after this model is clear.

A role is just one input.

It is not the decision.


Sources

  • OWASP Authorization Cheat Sheet — validate permissions on every request, deny-by-default, least privilege, auditability.
  • OWASP Top 10 2021 A01 Broken Access Control — access control failures and principle of least privilege.
  • NIST SP 800-162 — ABAC model using subject, object, operation, and environment attributes.
  • NIST RBAC model publications — users, roles, permissions, sessions, hierarchies, and constraints.
Lesson Recap

You just completed lesson 41 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.