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:
MVC controllers and actions
SignalR hubs and hub methods
Blazor components
Minimal API endpoints
Shared authorization requirements and handlers
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:
MVC
SignalR
Blazor
Endpoint routing
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 |
| Standard role/claim/policy declarations |
| 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
Define authorization requirements around business capabilities.
Keep authorization handlers independent of MVC, SignalR, and Blazor whenever possible.
Use custom attributes when requirement data needs to be declared close to the protected operation.
Use named policies for broadly reusable static policies.
Use resource authorization when access depends on the specific resource.
Never rely on Blazor UI authorization as the only security boundary.
Avoid embedding business logic directly in authorization attributes.
Test handlers independently from framework-specific integration tests.
Be careful when inspecting
AuthorizationHandlerContext.Resource.Keep tenant and ownership checks explicit in multi-tenant applications.
Apply authorization to both HTTP and SignalR operations when both expose the same protected capability.
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.
Join the conversation! Your thoughts help the community grow.