In the previous article, we learned how to use Policy-Based Authorization to protect API endpoints based on claims such as a user's membership level.

Policies work well when the rule is simple—for example:

But real business applications often require more complex rules.

For example:

These rules cannot be implemented using built-in role or claim checks alone.

This is where Custom Authorization Handlers become useful.

In this article, you'll learn how custom authorization works, when to use it, and how to implement your own authorization handler in ASP.NET Core.

Custom Authorization in ASP.NET Core Creating Your Own Authorization Handler

What Is a Custom Authorization Handler?

A Custom Authorization Handler allows you to write your own authorization logic.

Instead of checking only a role or claim, your code decides whether the current user is allowed to perform a specific action.

Think of it as creating your own security rule.

ASP.NET Core simply asks:

"Does this request satisfy the custom requirement?"

Your handler provides the answer.

Why Do We Need Custom Authorization?

Let's continue with our SecureShop API.

Suppose customers are allowed to cancel orders.

However, the business introduces a new rule:

Orders can only be cancelled before they are shipped.

Can we solve this using roles?

❌ No.

Can we solve this using claims?

❌ No.

The decision depends on the current order's status, which is business data.

This is exactly the type of problem that a custom authorization handler is designed to solve.

How Custom Authorization Works

A custom authorization solution has three parts.

Authorization Requirement
          │
          ▼
Authorization Handler
          │
          ▼
Authorization Policy
          │
          ▼
Protected Endpoint

Each part has a different responsibility.

ComponentResponsibility
RequirementDefines what needs to be checked
HandlerContains the business logic
PolicyConnects the requirement to ASP.NET Core

Step 1: Create an Authorization Requirement

A requirement represents a rule.

Create a new class.

using Microsoft.AspNetCore.Authorization;

public class CanCancelOrderRequirement : IAuthorizationRequirement
{
}

Notice that the class doesn't contain any code.

Its purpose is simply to represent the authorization requirement.

Step 2: Create the Authorization Handler

Now create the handler.

using Microsoft.AspNetCore.Authorization;

public class CanCancelOrderHandler
    : AuthorizationHandler<CanCancelOrderRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        CanCancelOrderRequirement requirement)
    {
        var orderStatus =
            context.User.FindFirst("OrderStatus")?.Value;

        if (orderStatus == "Pending")
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The important part is:

context.Succeed(requirement);

Calling this method tells ASP.NET Core:

The authorization requirement has been satisfied.

If it isn't called, authorization fails automatically.

Step 3: Register the Policy

Open Program.cs.

Register the policy.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CancelOrderPolicy", policy =>
    {
        policy.Requirements.Add(
            new CanCancelOrderRequirement());
    });
});

This tells ASP.NET Core which requirement belongs to the policy.


Step 4: Register the Authorization Handler

Next, register the handler.

builder.Services.AddSingleton<
    IAuthorizationHandler,
    CanCancelOrderHandler>();

Now ASP.NET Core knows which handler should evaluate the requirement.

Step 5: Protect the Endpoint

Apply the policy to an endpoint.

[Authorize(Policy = "CancelOrderPolicy")]
[HttpDelete("{id}")]
public IActionResult CancelOrder(int id)
{
    return Ok("Order Cancelled");
}

Before executing this action, ASP.NET Core automatically invokes the custom authorization handler.

Internal Authorization Flow

Let's see what happens internally.

Client
   │
   │ JWT
   ▼
Authentication Middleware
   │
   ▼
Validate JWT
   │
   ▼
ClaimsPrincipal
   │
   ▼
Policy Evaluation
   │
   ▼
Custom Authorization Handler
   │
   ▼
Business Rule Satisfied?
      │
 Yes  │ No
      │
      ▼
Controller Executes

Unlike role or claim checks, the decision is made entirely by your own code.

Practical Example

Let's continue with the SecureShop API.

Suppose two customers attempt to cancel an order.

Customer 1

Order Status : Pending

The handler checks:

Pending == Pending

The requirement succeeds.

API returns:

200 OK

Customer 2

Order Status : Shipped

The handler checks:

Shipped == Pending

The requirement fails.

API returns:

403 Forbidden

Although both users are authenticated, only one satisfies the business rule.

Real-World Use Cases

Custom authorization handlers are commonly used for rules such as:

Whenever authorization depends on application data or business logic, a custom handler is usually the right choice.

Common Mistakes

Mistake 1: Putting Authorization Logic Inside Controllers

Some developers write code like this:

if(order.Status != "Pending")
{
    return Forbid();
}

This scatters authorization rules across multiple controllers.

Instead, move the rule into a custom authorization handler so it can be reused.

Mistake 2: Using Custom Handlers for Simple Role Checks

If the rule is simply:

User must be an Admin

then use:

[Authorize(Roles = "Admin")]

Don't create a custom handler for something that ASP.NET Core already supports.

Mistake 3: Mixing Business Logic and Authorization Logic

A handler should decide whether a user can perform an action.

It should not update the database, modify orders, or send emails.

Keep authorization focused on access decisions only.


Mistake 4: Forgetting to Register the Handler

Creating the requirement and handler isn't enough.

If the handler isn't registered with Dependency Injection, ASP.NET Core will never execute it.

Key Takeaways

At this point, our SecureShop API supports authentication, role-based authorization, policy-based authorization, and custom authorization rules. In the next article, we'll begin integrating a real database by replacing our hardcoded login with Entity Framework Core, allowing users to authenticate using data stored in SQL Server.