Authorization becomes difficult to maintain when the same security rule has to be implemented separately across multiple ASP.NET Core technologies.

A typical enterprise application may contain:

Before .NET 11, custom authorization metadata based on IAuthorizationRequirementData had more limited framework coverage. In .NET 11, the same requirement-data-based authorization model can be applied to MVC controllers and actions, SignalR hubs and hub methods, and Blazor authorization components.

This makes it possible to define authorization requirements closer to the authorization attribute itself while reusing the same requirement and handler implementation across different application surfaces.

The result is a more consistent authorization architecture:

                    Shared Authorization Rule
                             |
              +--------------+--------------+
              |              |              |
              v              v              v
             MVC          SignalR         Blazor
              |              |              |
              +--------------+--------------+
                             |
                             v
                  Authorization Handler

The important part is that sharing metadata does not mean blindly sharing every authorization decision. MVC, SignalR, and Blazor have different execution models, so resource-specific authorization may still require framework-aware handling.

Why Shared Authorization Metadata Matters

Consider an application that has an Editor requirement.

The application may expose the same business capability through:

MVC:
POST /articles/{id}/publish

SignalR:
ArticleHub.PublishArticle(id)

Blazor:
Publish button/component

Without a shared authorization model, developers might create:

MVC policy
SignalR policy
Blazor policy

Each implementation can eventually drift.

For example, MVC might check:

Editor role

while SignalR checks:

Editor claim

and Blazor checks:

CanPublish flag

The application now has three different interpretations of the same business rule.

A shared requirement provides a stronger boundary:

PublishArticleRequirement
        |
        +--> Authorization Handler
        |
        +--> MVC
        |
        +--> SignalR
        |
        +--> Blazor

What Is IAuthorizationRequirementData?

IAuthorizationRequirementData allows an authorization attribute to provide one or more authorization requirements.

A custom attribute can implement:

public interface IAuthorizationRequirementData
{
    IEnumerable<IAuthorizationRequirement> GetRequirements();
}

A requirement represents something that must be satisfied before access is granted.

For example:

public sealed class MinimumAgeRequirement(int age)
    : IAuthorizationRequirement
{
    public int Age { get; } = age;
}

An attribute can then provide that requirement:

public sealed class MinimumAgeAuthorizeAttribute(int age)
    : AuthorizeAttribute,
      IAuthorizationRequirement,
      IAuthorizationRequirementData
{
    public int Age { get; } = age;

    public IEnumerable<IAuthorizationRequirement>
        GetRequirements()
    {
        yield return this;
    }
}

The important idea is that the attribute carries the requirement data.

The authorization system can then evaluate the requirement through a normal authorization handler.

Creating a Shared Requirement

For a real application, consider a business rule such as:

A user must have permission to publish content.

Create a requirement:

public sealed class CanPublishRequirement
    : IAuthorizationRequirement
{
}

The requirement itself does not need to know where authorization is being performed.

It simply represents the business rule.

Now create an authorization handler:

public sealed class CanPublishHandler
    : AuthorizationHandler<CanPublishRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        CanPublishRequirement requirement)
    {
        if (context.User.HasClaim(
                "permission",
                "content.publish"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Register the handler:

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

The same requirement can now be used by different application surfaces.

Creating a Requirement-Based Attribute

A reusable attribute can encapsulate the requirement:

public sealed class CanPublishAttribute
    : AuthorizeAttribute,
      IAuthorizationRequirementData
{
    public IEnumerable<IAuthorizationRequirement>
        GetRequirements()
    {
        yield return new CanPublishRequirement();
    }
}

The attribute can now be applied directly to supported endpoints or components.

This keeps authorization declarations readable:

[CanPublish]
public IActionResult Publish(int id)
{
    // Publishing logic.
}

The authorization rule is declared at the point where access is being protected.

Using the Metadata with MVC

MVC controllers and actions can use the custom attribute.

For example:

public class ArticlesController : Controller
{
    [CanPublish]
    [HttpPost]
    public IActionResult Publish(int id)
    {
        return Ok();
    }
}

The MVC action does not need to know how the permission is calculated.

The authorization pipeline evaluates the requirement before the action is allowed to execute.

This is preferable to writing authorization logic directly inside the controller:

if (!User.HasClaim(
        "permission",
        "content.publish"))
{
    return Forbid();
}

The latter approach mixes authorization policy logic with application behavior.

Using the Same Requirement with SignalR

SignalR hubs can also use authorization attributes.

For example:

public class ArticleHub : Hub
{
    [CanPublish]
    public async Task PublishArticle(int articleId)
    {
        await PublishAsync(articleId);
    }
}

The hub method can therefore use the same authorization metadata as an MVC action.

This is particularly useful when an application exposes the same business capability through both HTTP and real-time APIs.

For example:

HTTP:
POST /articles/10/publish

SignalR:
PublishArticle(10)

Both can require:

CanPublishRequirement

Using Authorization Metadata with Blazor

.NET 11 also extends this model to Blazor authorization scenarios involving AuthorizeView and AuthorizeRouteView.

For a routed component, authorization can be declared with an authorization attribute:

@page "/articles/{id:int}"
@attribute [CanPublish]

<h1>Article</h1>

For conditional UI, AuthorizeView can be used with appropriate authorization metadata.

However, there is an important distinction between hiding UI and enforcing authorization.

A Blazor component should not be treated as the ultimate security boundary merely because an authorization check controls whether a button is rendered.

The server-side operation that actually changes protected data must still enforce authorization.

UI Authorization Is Not Data Security

Consider:

<AuthorizeView>
    <Authorized>
        <button @onclick="Publish">
            Publish
        </button>
    </Authorized>
</AuthorizeView>

This improves the user experience because unauthorized users don't see the button.

But hiding a button does not protect the underlying operation.

A malicious client can still attempt to call the server-side API or SignalR method directly.

The actual operation should therefore remain protected:

[CanPublish]
public async Task PublishArticle(int articleId)
{
    await articleService.PublishAsync(articleId);
}

The general rule is:

Blazor UI check
       |
       v
User experience

Server authorization
       |
       v
Actual security boundary

Both can be useful, but they serve different purposes.

Sharing the Authorization Handler

The strongest advantage of requirement-based authorization is that the core rule can remain shared.

For example:

public sealed class CanPublishHandler
    : AuthorizationHandler<CanPublishRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        CanPublishRequirement requirement)
    {
        var hasPermission =
            context.User.HasClaim(
                "permission",
                "content.publish");

        if (hasPermission)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The handler can be used regardless of whether authorization originated from:

This provides one central definition of the permission.

When Resource Authorization Is Required

Not every authorization decision can be based solely on user claims.

Consider:

User: alice
Permission: article.edit
Article: 123
Owner: bob

The user may have a general editing permission, but the application might restrict editing to articles owned by the current user.

Now authorization depends on both:

User + Resource

The requirement may therefore need resource information.

For example:

public sealed class CanEditArticleRequirement
    : IAuthorizationRequirement
{
}

The handler can inspect the resource when it is available:

public sealed class CanEditArticleHandler
    : AuthorizationHandler<CanEditArticleRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        CanEditArticleRequirement requirement)
    {
        if (context.Resource is Article article &&
            article.OwnerId == context.User.FindFirst("sub")?.Value)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The exact resource passed to the handler depends on the framework and authorization execution path.

Resource Types Can Differ

This is an important architectural consideration.

Authorization handlers can receive different resource objects depending on where authorization is performed.

For example, endpoint routing commonly provides access to HttpContext through the authorization resource.

MVC can also involve framework-specific authorization contexts.

Blazor can provide route information through its authorization infrastructure.

Therefore, avoid writing a handler that blindly assumes one framework-specific resource type.

This is fragile:

var httpContext =
    (HttpContext)context.Resource!;

A safer approach is:

if (context.Resource is HttpContext httpContext)
{
    // Use HTTP-specific information.
}

Or isolate framework-specific resource extraction behind a dedicated abstraction.

Creating a Shared Policy Model

A scalable application can organize authorization into three layers:

Authorization Metadata
        |
        v
Requirements
        |
        v
Handlers

For example:

Authorization/
    Requirements/
        CanPublishRequirement.cs
        CanEditArticleRequirement.cs

    Handlers/
        CanPublishHandler.cs
        CanEditArticleHandler.cs

    Attributes/
        CanPublishAttribute.cs
        CanEditArticleAttribute.cs

This makes it easier to locate and test authorization rules.

The application surface then simply declares the required capability.

Attribute-Based vs Named Policies

ASP.NET Core also supports conventional named policies.

For example:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(
        "CanPublish",
        policy =>
        {
            policy.RequireClaim(
                "permission",
                "content.publish");
        });
});

MVC can use:

[Authorize(Policy = "CanPublish")]
public IActionResult Publish(int id)
{
    return Ok();
}

An endpoint can use:

app.MapPost(
    "/articles/{id}/publish",
    PublishArticle)
    .RequireAuthorization("CanPublish");

The requirement-data approach offers another way to associate requirements directly with custom authorization attributes.

A useful rule is:

Approach

Best For

Named policy

Central policies reused by name

AuthorizeAttribute

Standard role/claim/policy declarations

IAuthorizationRequirementData

Custom attributes carrying requirement data

Resource authorization

Decisions based on a specific resource

Authorization handler

Central implementation of a requirement

These approaches are complementary rather than mutually exclusive.

Dynamic Requirement Data

One particularly useful feature of IAuthorizationRequirementData is that the attribute can carry parameters.

Consider a minimum-age rule:

[MinimumAgeAuthorize(21)]
public IActionResult Purchase()
{
    return Ok();
}

The attribute can create a requirement containing the configured value:

public sealed class MinimumAgeRequirement(int age)
    : IAuthorizationRequirement
{
    public int Age { get; } = age;
}

The handler can then evaluate it:

public sealed class MinimumAgeHandler
    : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var birthDateClaim =
            context.User.FindFirst("birthdate");

        if (birthDateClaim is null)
        {
            return Task.CompletedTask;
        }

        if (!DateTime.TryParse(
                birthDateClaim.Value,
                out var birthDate))
        {
            return Task.CompletedTask;
        }

        var age = DateTime.UtcNow.Year -
                  birthDate.Year;

        if (birthDate.Date >
            DateTime.UtcNow.Date.AddYears(-age))
        {
            age--;
        }

        if (age >= requirement.Age)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The same attribute pattern can be used for requirements such as:

MinimumRoleLevel
MinimumSubscriptionLevel
RequiredPermission
RequiredTenant
RequiredAccountType

Multiple Requirements

An authorization policy can contain multiple requirements.

For example:

public sealed class CanPublishArticleRequirement
    : IAuthorizationRequirement
{
}

public sealed class CanPublishInTenantRequirement
    : IAuthorizationRequirement
{
}

An attribute can return both:

public IEnumerable<IAuthorizationRequirement>
    GetRequirements()
{
    yield return new CanPublishArticleRequirement();
    yield return new CanPublishInTenantRequirement();
}

This allows the authorization system to evaluate multiple conditions.

Conceptually:

Can Publish?
      |
      +--> Has Publish Permission?
      |
      +--> Belongs to Required Tenant?
      |
      +--> Account Is Active?

A request is authorized only when the resulting authorization policy succeeds.

MVC, SignalR, and Blazor in One Application

Consider a content-management system with all three technologies.

MVC

[CanPublish]
public IActionResult Publish(int id)
{
    return Ok();
}

SignalR

[CanPublish]
public Task PublishArticle(int id)
{
    return articleService.PublishAsync(id);
}

Blazor

@attribute [CanPublish]

<h2>Publish Article</h2>

All three use:

CanPublishAttribute
        |
        v
CanPublishRequirement
        |
        v
CanPublishHandler

This eliminates a large amount of duplicated policy logic.

Common Mistakes

Treating UI Authorization as Server Security

A hidden Blazor button is not a security boundary.

Always enforce authorization at the server operation.

Assuming All Resources Are HttpContext

A shared handler can be invoked from different frameworks.

Don't assume that context.Resource always contains the same object type.

Duplicating Authorization Logic

Avoid implementing:

MVC permission logic
SignalR permission logic
Blazor permission logic

when the underlying business rule is identical.

Centralize the requirement and handler.

Putting Business Logic in Attributes

Attributes should describe authorization requirements.

Avoid placing database queries, complex business decisions, or service calls directly inside attribute definitions.

Keep that logic in authorization handlers or supporting services.

Forgetting Multi-Tenant Boundaries

A user may have a permission but still not be authorized to access a specific tenant's resource.

Authorization requirements should consider tenant boundaries where applicable.

Testing Shared Authorization

A shared requirement should be unit tested independently of the framework.

For example:

[Fact]
public async Task UserWithPublishPermission_IsAuthorized()
{
    var user = new ClaimsPrincipal(
        new ClaimsIdentity(
        [
            new Claim(
                "permission",
                "content.publish")
        ]));

    var requirement =
        new CanPublishRequirement();

    var context =
        new AuthorizationHandlerContext(
            [requirement],
            user,
            null);

    var handler =
        new CanPublishHandler();

    await handler.HandleAsync(context);

    Assert.True(context.HasSucceeded);
}

Then add integration tests for each application surface:

MVC Test
   |
   v
SignalR Test
   |
   v
Blazor Test
   |
   v
Shared Handler Tests

This helps distinguish framework integration problems from authorization-rule problems.

Advantages

One Authorization Rule

A business permission can be represented by one requirement and handler.

Less Duplication

MVC, SignalR, and Blazor don't need independent implementations of the same authorization logic.

Better Maintainability

Changing a permission rule can be done centrally.

Parameterized Requirements

Custom attributes can carry requirement-specific values.

Consistent Security Model

Different application surfaces can enforce the same conceptual permissions.

Limitations and Considerations

Framework Context Still Matters

Sharing requirements does not eliminate framework-specific authorization behavior.

Resource Authorization Can Differ

The resource supplied to the authorization handler depends on the execution environment.

Blazor Client Code Is Not a Security Boundary

Client-side UI checks must be backed by server-side enforcement.

Complex Policies Need Careful Design

A single handler containing every authorization rule can become difficult to maintain.

Separate requirements and handlers according to business capabilities.

Recommended Architecture

For a larger ASP.NET Core application, a useful structure is:

Application
|
+-- Authorization
|   |
|   +-- Requirements
|   |   +-- CanPublishRequirement
|   |   +-- CanEditRequirement
|   |   +-- CanDeleteRequirement
|   |
|   +-- Handlers
|   |   +-- CanPublishHandler
|   |   +-- CanEditHandler
|   |   +-- CanDeleteHandler
|   |
|   +-- Attributes
|       +-- CanPublishAttribute
|       +-- CanEditAttribute
|
+-- MVC
|
+-- SignalR
|
+-- Blazor
|
+-- Minimal APIs

The application surfaces consume authorization metadata while the authorization layer owns the security rules.

Best Practices

  1. Define authorization requirements around business capabilities.

  2. Keep authorization handlers independent of MVC, SignalR, and Blazor whenever possible.

  3. Use custom attributes when requirement data needs to be declared close to the protected operation.

  4. Use named policies for broadly reusable static policies.

  5. Use resource authorization when access depends on the specific resource.

  6. Never rely on Blazor UI authorization as the only security boundary.

  7. Avoid embedding business logic directly in authorization attributes.

  8. Test handlers independently from framework-specific integration tests.

  9. Be careful when inspecting AuthorizationHandlerContext.Resource.

  10. Keep tenant and ownership checks explicit in multi-tenant applications.

  11. Apply authorization to both HTTP and SignalR operations when both expose the same protected capability.

  12. Treat client-side authorization primarily as a user-experience concern unless the authorization executes on a trusted server.

Conclusion

ASP.NET Core 11 makes it easier to share custom authorization metadata across different parts of an application.

With IAuthorizationRequirementData, a custom authorization attribute can provide requirements that are evaluated by the standard ASP.NET Core authorization infrastructure. In .NET 11, this model extends beyond routed endpoints to MVC controllers and actions, SignalR hubs and hub methods, and Blazor authorization scenarios.

The architectural benefit is significant:

             Authorization Attribute
                       |
                       v
             Authorization Requirement
                       |
                       v
               Authorization Handler
                       |
          +------------+------------+
          |            |            |
          v            v            v
         MVC         SignalR      Blazor

This approach helps prevent authorization rules from becoming scattered across controllers, hubs, components, and client-side code.

The most important design principle remains simple: share the authorization rule, but respect the execution model of each framework.

Use common requirements and handlers for common business rules, use resource authorization when the decision depends on the protected resource, and always enforce security at the trusted server boundary.