When ASP.NET Core Authorization Is Enough
Pattern classification: Alternative Pattern
Difficulty: Intermediate
Prerequisites: Policy Context and Explicit Decision Outcomes
Terminology note: The Learning vocabulary used in this comparison is mapped to established authorization, ABAC, workflow, capability, provenance, and mediation concepts in Terminology and Established Architecture Concepts.
ASP.NET Core already includes a capable authorization system.
For many applications, that built-in system is not merely an acceptable alternative to a broader governance pipeline.
It is the better design.
This comparison asks a narrower architectural question:
When should an application use ordinary ASP.NET Core authorization policies and handlers, and when does the problem become larger than authorization?
The goal is not to make one model defeat the other.
The goal is to identify the boundary.
Start with the Main Difference
ASP.NET Core authorization primarily answers:
May this user access or perform this operation on this resource?
A governed-execution pipeline may need to answer a larger lifecycle question:
What should happen next with this proposed consequential operation?
Those questions overlap, but they are not identical.
The difference matters because architecture should match the problem being solved.
┌──────────────────────────────┐
│ Authentication │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ ASP.NET Core Authorization │
│ "May this actor perform │
│ this operation on this │
│ resource?" │
└───────────────┬──────────────┘
│
├── Denied ──→ Reject / End
│
│ Authorized
▼
┌──────────────────────────────┐
│ Governance / Workflow │
│ "What should happen next │
│ with this proposed │
│ consequential operation?" │
│ │
│ Outcomes may include: │
│ • Allow │
│ • Deny │
│ • Defer │
│ • AcknowledgmentRequired │
│ • EscalationRecommended │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ Host-Owned Execution │
│ (Immediate or deferred) │
└──────────────────────────────┘
What ASP.NET Core Authorization Already Provides
ASP.NET Core supports:
- Role-based authorization.
- Claims-based authorization.
- Named authorization policies.
- Custom authorization requirements.
- One or more handlers for a requirement.
- Imperative authorization through
IAuthorizationService. - Resource-based authorization when a decision depends on a loaded domain resource.
- Authorization failure reasons.
- Integration with endpoint routing, MVC, Razor Pages, Blazor, and dependency injection.
Policies are composed from requirements, and handlers evaluate those requirements against an AuthorizationHandlerContext.
When multiple requirements are placed in one policy, all requirements must succeed for authorization to succeed.
For resource-specific decisions, the application can load the resource and invoke IAuthorizationService directly.
That is already a strong, reusable, testable model.
A Built-In Authorization Version of the Account Example
The foundational policy-context tutorial uses an account-disable operation to demonstrate explicit context and decision outcomes.
A perfectly reasonable ASP.NET Core version can use resource-based authorization instead.
First, define the resource:
public sealed record DisableAccountResource(
string AccountId,
string TenantId,
bool IsProtected);
Then define an authorization requirement:
using Microsoft.AspNetCore.Authorization;
public sealed class DisableAccountRequirement
: IAuthorizationRequirement;
The handler can evaluate the authenticated actor and loaded account:
using Microsoft.AspNetCore.Authorization;
public sealed class DisableAccountHandler
: AuthorizationHandler<
DisableAccountRequirement,
DisableAccountResource>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DisableAccountRequirement requirement,
DisableAccountResource resource)
{
if (!context.User.IsInRole("Administrator"))
{
context.Fail(
new AuthorizationFailureReason(
this,
"Administrator role is required."));
return Task.CompletedTask;
}
string? actorTenant =
context.User.FindFirst("tenant_id")?.Value;
if (!string.Equals(
actorTenant,
resource.TenantId,
StringComparison.Ordinal))
{
context.Fail(
new AuthorizationFailureReason(
this,
"Cross-tenant account changes are not allowed."));
return Task.CompletedTask;
}
if (resource.IsProtected)
{
context.Fail(
new AuthorizationFailureReason(
this,
"Protected accounts cannot be disabled through this path."));
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Register the policy:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"CanDisableAccount",
policy =>
policy.Requirements.Add(
new DisableAccountRequirement()));
});
builder.Services.AddSingleton<
IAuthorizationHandler,
DisableAccountHandler>();
The endpoint can load the resource before evaluating authorization:
app.MapPost(
"/accounts/{accountId}/disable",
async (
string accountId,
ClaimsPrincipal user,
IAuthorizationService authorization,
IAccountRepository accounts,
IAccountService accountService,
CancellationToken cancellationToken) =>
{
Account account =
await accounts.GetAsync(
accountId,
cancellationToken);
var resource =
new DisableAccountResource(
account.Id,
account.TenantId,
account.IsProtected);
AuthorizationResult result =
await authorization.AuthorizeAsync(
user,
resource,
"CanDisableAccount");
if (!result.Succeeded)
{
return Results.Forbid();
}
await accountService.DisableAsync(
accountId,
cancellationToken);
return Results.NoContent();
});
For this problem, the design is clear:
Authenticated user
↓
Loaded resource
↓
Authorization policy
↓
Succeeded / Failed
↓
Host executes or rejects
There is no reason to introduce a broader governance abstraction merely to avoid using the framework already designed for this job.
When ASP.NET Core Authorization Wins
Use the built-in authorization model when most of the following are true.
1. The Question Is Fundamentally Access Control
The system needs to determine whether an authenticated principal may:
- Reach an endpoint.
- View a page.
- Modify a resource.
- Invoke an operation.
- Access data.
That is authorization's natural problem domain.
2. Success and Failure Are Sufficient
The host needs a result equivalent to:
Authorized
Not authorized
The application may preserve failure reasons, log details, or map failures differently at the transport layer.
But it does not need authorization itself to represent a larger workflow state.
3. The Decision Belongs to the Current Request
The authorization decision is consumed immediately.
For example:
Request
↓
Authorize
↓
Execute now or return 403
There is no durable pause between decision and execution.
4. The User and Resource Are the Main Inputs
ASP.NET Core authorization is especially natural when policy evaluates:
- Claims.
- Roles.
- Authentication state.
- A resource supplied to the handler.
- Other application services injected into the handler.
Resource-based authorization is specifically intended for cases where the resource must be loaded before the decision can be made.
5. Execution Authority Does Not Need Its Own Artifact
The application does not need to issue a separate, short-lived capability that can later be validated by another component.
The successful authorization check and the application's existing identity/session model are enough.
6. The Application Benefits from Framework Integration
ASP.NET Core authorization already integrates with:
[Authorize].- Endpoint
RequireAuthorization. IAuthorizationService.- Dependency injection.
- Authentication principals.
- Framework middleware.
Using those built-in surfaces reduces custom infrastructure and makes the design familiar to .NET developers.
Where the Problem Becomes Larger Than Authorization
The boundary appears when a proposal does not fit cleanly into success or failure.
Consider these outcomes:
Allowed
Denied
Deferred
AcknowledgmentRequired
EscalationRecommended
Denied maps naturally to an authorization failure.
The other states do not all mean "not authorized."
Deferred Is a Workflow State
Suppose the actor is authorized, but a maintenance hold means the action should be retried later.
Authorized actor
+
Temporary operational hold
↓
Deferred
Calling this an authorization failure can erase an important distinction.
Acknowledgment Required Is an Interruption Boundary
The actor may already be authorized.
The system may still require an explicit acknowledgment before the operation continues.
Authorized
↓
Pause
↓
Acknowledgment
↓
Re-evaluate
That is a multi-step governance lifecycle, not simply an access-control check.
Escalation Recommended Routes Work Elsewhere
A protected resource may require review by a different authority.
The current actor may not be rejected permanently.
Instead:
Current path
↓
Escalate to higher-authority path
Again, this is richer than authorized versus unauthorized.
Scoped Post-Approval Authority May Outlive the Request
A consequential workflow may authorize an operation now but execute it later or in another component.
That introduces questions such as:
- Which actor was approved?
- Which exact operation?
- Which exact resource?
- For which audience?
- Until when?
- Under which policy version?
- Has the authority already been consumed?
At that point, a narrow capability can become a useful artifact distinct from the original authorization result.
Authorization Failure Reasons Are Useful, but They Are Not Workflow Outcomes
ASP.NET Core can preserve authorization failure reasons.
That is important and should not be ignored when comparing the models.
An application can use those reasons for diagnostics or custom result handling.
But a failure reason still explains a failed authorization result.
It does not by itself turn the authorization result into a generalized state machine containing:
Deferred
AcknowledgmentRequired
EscalationRecommended
An application can build those semantics around authorization.
Once it does, however, that additional lifecycle is application architecture layered beside the authorization system.
⚠️ Anti-Pattern: Workflow Logic Hidden Inside Authorization
public sealed class DisableAccountHandler
: AuthorizationHandler<DisableAccountRequirement, Account>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DisableAccountRequirement requirement,
Account resource)
{
// 1. Correct: basic access control
if (!context.User.IsInRole("Administrator"))
{
context.Fail(new AuthorizationFailureReason(
this, "Administrator role required."));
return Task.CompletedTask;
}
// 2. ❌ Wrong boundary: operational deferral encoded as authorization failure
if (resource.IsUnderMaintenanceHold)
{
context.Fail(new AuthorizationFailureReason(
this, "Deferred: maintenance hold."));
return Task.CompletedTask;
}
// 3. ❌ Wrong boundary: escalation workflow encoded as authorization failure
if (resource.IsHighlySensitive)
{
context.Fail(new AuthorizationFailureReason(
this, "EscalationRecommended: requires senior approval."));
return Task.CompletedTask;
}
// 4. ❌ Wrong boundary: acknowledgment lifecycle encoded as authorization failure
if (!context.User.HasClaim("AcknowledgedRisk", "true"))
{
context.Fail(new AuthorizationFailureReason(
this, "AcknowledgmentRequired: user must confirm risk."));
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
The Hybrid Pattern Is Often the Best Answer
These approaches do not need to be mutually exclusive.
A common design is:
Authentication
↓
ASP.NET Core Authorization
|
| "May this actor enter this operation?"
↓
Governance / workflow evaluation
|
| "What should happen next?"
↓
Acknowledgment / escalation / capability when required
↓
Host-owned execution
For example, ASP.NET Core authorization may determine that the caller is permitted to request an account-disable operation.
A separate governance flow may then determine that:
- The operation is temporarily deferred.
- A protected account requires escalation.
- A sensitive change requires acknowledgment.
- A later worker needs a short-lived capability before executing.
That separation keeps authentication and authorization in the framework that already handles them well while reserving richer governance semantics for cases that actually need them.
Comparison Matrix
| Concern | ASP.NET Core authorization | Governed-execution model |
|---|---|---|
| Primary question | May this user access or perform this operation? | What should happen next with this proposed operation? |
| Natural result | Success / failure | Allow / deny / defer / acknowledge / escalate |
| Roles and claims | First-class | Usually consumed as context, not replaced |
| Resource-aware checks | Supported through imperative/resource-based authorization | Supported through explicit policy context |
| Failure reasons | Supported | Structured reasons are typically part of every decision |
| Request-local authorization | Excellent fit | Often unnecessary overhead if this is the whole problem |
| Durable acknowledgment lifecycle | Application-defined outside basic authorization | First-class architectural concern |
| Escalation routing | Application-defined | Can be represented explicitly as an outcome |
| Deferred decision | Application-defined | Can be represented explicitly as an outcome |
| Post-approval scoped capability | Not the core authorization result model | May be first-class when execution is separated from approval |
| Policy identity in durable decision evidence | Application-defined | Common governance requirement |
| Framework integration | Native ASP.NET Core | Additional application/framework architecture |
| Operational complexity | Lower when access control is the real problem | Higher, justified only when the workflow needs it |
A Decision Guide
Prefer ASP.NET Core authorization when:
The actor is authenticated
+
The resource is known
+
The question is access control
+
Success/failure is enough
+
Execution follows immediately
Prefer a richer governed-execution model when:
The proposal has multiple meaningful non-final states
or
Approval and execution are separated in time or component
or
Acknowledgment or escalation is part of the lifecycle
or
Execution authority must be narrowly scoped after approval
or
Durable decision lineage is a core requirement
Use both when authorization determines entry to the operation and governance determines the later lifecycle.
Do Not Build a Governance Pipeline Just to Be Consistent
Architectural consistency is useful.
Unnecessary machinery is not.
If the requirement is:
Only administrators may access this endpoint.
then:
[Authorize(Roles = "Administrator")]
may be the correct answer.
If the requirement is:
Users with this claim may access this resource.
a claims or policy requirement may be enough.
If the requirement is:
The current user may modify this specific document only when the resource satisfies these conditions.
resource-based authorization may be enough.
The governed-execution model becomes valuable when the architectural problem genuinely includes lifecycle, authority, acknowledgment, escalation, or evidence beyond ordinary authorization.
Questions to Ask Before Choosing
- Is the real question authorization, or is it workflow governance?
- Does the decision need more than success or failure?
- Is
Deferredmeaningfully different fromDenied? - Can an already-authorized actor still be required to acknowledge something?
- Can the operation be routed for escalation rather than rejected?
- Does execution happen in the same request that made the decision?
- Must approval create narrow authority for a later component?
- Must the system preserve a durable record of policy identity and decision lineage?
- Would built-in ASP.NET Core policies express the requirement with less custom machinery?
- Are you adding a governance pipeline because the problem needs it, or because the architecture already has one?
The last question is especially important.
Use the smallest architecture that preserves the boundaries you actually need.
Relationship to the Foundational Tutorial
Policy Context and Explicit Decision Outcomes demonstrates a broader decision vocabulary because its teaching problem includes:
Allowed
Denied
Deferred
AcknowledgmentRequired
EscalationRecommended
That tutorial should not be read as a claim that ASP.NET Core authorization is insufficient for ordinary authorization.
Instead:
ASP.NET Core authorization
↓
Excellent fit for access-control decisions
Governed decision model
↓
Useful when the lifecycle itself has additional states
Both patterns can be correct.
Official ASP.NET Core References
For the framework behavior discussed in this comparison, see:
- Introduction to authorization in ASP.NET Core
- Policy-based authorization in ASP.NET Core
- Resource-based authorization in ASP.NET Core
AuthorizationHandlerContext
Related Content
- Architecture — return to the architecture learning area.
- Policy Context and Explicit Decision Outcomes — compare the richer governance-decision model directly.
- Decision Before Execution — revisit the separation between decision and host-owned execution.
- Build a Governed API Operation — practice the hybrid boundary by keeping ASP.NET Core authorization while adding explicit governance, acknowledgment, scoped authority, host-owned execution, and invariant tests.
- Scoped Capability and Host-Owned Execution — see when authority becomes a separate, narrowly scoped artifact after approval.
- Governed AI Tool Gateway — see why AI-proposed consequential operations often require more than endpoint authorization alone.
Read it. Run it. Question it. Improve it.