Authorization Policy TagHelpers for ASP.NET Core Razor Views
An essential part of ASP.NET Core’s authentication and authorization implementation is the ClaimsPrincipal
class. Duende IdentityServer issues claims using the OpenID Connect protocol, and your ASP.NET Core applications transform the results into a ClaimsPrincipal
instance used throughout your web applications.
Once you have an authenticated user, the fun starts, as you’ll want to make logical decisions about which resources the user can access. One of the more important places to make decisions is your user interface, where you may want to show elements based on authorization policies in your solution.
In this post, we’ll look at implementing an AuthorizationPolicyTagHelper
to keep your Razor Views free of noisy C# flow constructs and discuss performance considerations that can make your pages feel even snappier.
Before we get to the implementation details, we need to discuss the two crucial elements of this post: Tag Helpers and Authorization Policies. I’ll give an overview for folks who may be reading about these for the first time, but if you’re an experienced ASP.NET Core developer, you can skip down to the implementation.
What is a Tag Helper?
An ASP.NET Core Tag Helper is a server-side helper class that allows you to intercept the processing of HTML tags in your Razor views and alter the resulting output. By providing a more concise and declarative way to express implementation details, tag helpers can simplify and enhance the development of web applications.
Key features of ASP.NET Core Tag Helpers include:
- You can use Tag Helpers to modify the behavior of existing HTML elements.
- You can use Tag Helpers to add dynamic content to web pages.
- You can use Tag Helpers to incorporate server-side logic into web pages.
Tag Helpers are a powerful tool that you can use to improve the productivity and maintainability of ASP.NET Core applications.
What is an Authorization Policy?
Authorization policies allow developers to specify requirements that users must meet before granting access to a resource. ASP.NET Core includes standard requirements that fit many use cases, but developers can also implement custom policies and handlers. For this post, we’ll check that the role
claim exists on our ClaimsPrinicipal
instance.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("user", b =>
{
b.RequireAuthenticatedUser();
});
options.AddPolicy("admin", b =>
{
b.RequireAuthenticatedUser();
b.RequireClaim("role", "admin");
});
});
You can apply these policies to ASP.NET Core Razor pages and MVC endpoints by adding the Authorize
attribute and specifying the policy that needs to be matched.
[Authorize(policy: "user")]
public class IndexModel: PageModel
{
public void OnGet()
{
}
}
While this granularity is helpful, we usually need one additional layer of policy application in the Razor view, for example, to show or hide UI elements based on whether the current user matches a specific policy. Here’s how to verify the user against the policy, and use an if
to control the rendered HTML. Workable, but clunky.
@page
@using Microsoft.AspNetCore.Authorization
@model TopSecretApp.Pages.New
@inject IAuthorizationService Authorization
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>
Learn about <a href="https://duendesoftware.com">building Web apps with Duende</a>.
</p>
</div>
@{
var result = await Authorization.AuthorizeAsync(User, policyName: "user");
}
@if (result.Succeeded && User is {Identity.IsAuthenticated: true })
{
<div class="text-center">
<p>Hello, @User.Identity.Name!</p>
</div>
}
We can do better if we use ASP.NET Core’s Tag Helper approach!
Implementing an Authorization Policy Tag Helper
We aim to create an ASP.NET Core Tag Helper that keeps our views manageable while allowing us to show or hide page elements based on authorization policies. Let’s examine the final usage first, and then return to our Tag Helper code.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>
Learn about <a href="https://duendesoftware.com">building Web apps with Duende</a>.
</p>
</div>
<div class="row">
<div class="col">
<div class="text-center m-2" auth-policy="user">
<h2>Only For Users</h2>
<p>@Model.Message.Value</p>
<img src="https://placecats.com/300/300" alt="cat image placeholder"/>
</div>
</div>
<div class="col">
<div class="text-center m-2" auth-policy="admin">
<h2>Only For Admins</h2>
<p class="alert alert-info">Admins are also users</p>
<p>@Model.AdminMessage.Value</p>
<img src="https://placedog.net/300x300" alt="Dog image placeholder"/>
</div>
</div>
</div>
We will target any HTML tag with an auth-policy
attribute, the value of which is the name of the policy we want to test against. In the earlier code snippet, you’ll find two div
elements with an auth-policy="..."
attribute that triggers a Tag Helper to evaluate the policies defined in the previous section.
Let’s implement the code of the Tag Helper; you’ll be surprised by how straightforward it is.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace TopSecretApp.TagHelpers;
[HtmlTargetElement("*",
Attributes = AuthPolicyAttributeName,
TagStructure = TagStructure.Unspecified)]
public class AuthorizationPolicyTagHelper(
IAuthorizationService authorizationService,
ILogger<AuthorizationPolicyTagHelper> logger) : TagHelper
{
private const string AuthPolicyAttributeName = "auth-policy";
private const string AuthPolicyResourceAttributeName = "auth-policy-resource";
[Required]
[HtmlAttributeName(AuthPolicyAttributeName)]
public required string AuthorizationPolicyName { get; set; }
[HtmlAttributeName(AuthPolicyResourceAttributeName)]
public object? Resource { get; set; }
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; } = null!;
public override async Task ProcessAsync(
TagHelperContext context, TagHelperOutput output)
{
ArgumentException.ThrowIfNullOrEmpty(AuthorizationPolicyName);
// don't want the server-side attribute on the final html
output.Attributes.RemoveAll(AuthorizationPolicyName);
output.Attributes.RemoveAll(AuthPolicyResourceAttributeName);
var httpContext = ViewContext.HttpContext;
var result = await authorizationService.AuthorizeAsync(
httpContext.User,
Resource,
AuthorizationPolicyName);
if (!result.Succeeded)
{
logger.LogInformation("authorization for policy \"{policy}\" failed.", AuthorizationPolicyName);
output.SuppressOutput();
}
}
}
The IAuthorizationService
part of ASP.NET Core, does the heavy lifting in our AuthorizationPolicyTagHelper
implementation.NET Core. Once we run the policy, we get a result determining if the current user passes or fails the policy requirements. In the event of a failure, we choose to suppress all the output of this element, including all its child elements.
A fun exercise would be to modify the tag helper to allow you to choose between suppressing the output or adding a “disabled” style on the element so you could still, e.g. render a button that is displayed in a disabled state when the policy does not match.
We only need to register our new Tag Helper with our ASP.NET Core application, which you can do in the _ViewImports.cshtml
file in your Pages
or Views
folders. Remember to use the assembly name to register all helpers properly.
@addTagHelper *, TopSecretApp
Let’s examine what happens when we execute our ASP.NET Core application in all three authentication states: anonymous, user, and admin.
When we first visit the page, we see the welcome message as expected.
![Default view][../images/taghelpers-standard-view.png]
After logging in as a user
, we see the first element that requires the user
policy.
![View with user permissions][../images/taghelpers-user-view.png]
Finally, as an admin,
we can see all hidden elements on the page.
![View with admin permissions][../images/taghelpers-admin-view.png]
Performance Considerations
A few performance considerations exist when applying this Tag Helper in your ASP.NET Core Razor views.
Optimizing Data Retrieval
Since you pass models to most Razor views through a controller action or page handler, you may have already incurred a performance cost for retrieving that information. An expensive database call or a web request may waste resources if you don’t use that information to render the page.
Consider loading only the data needed for the current user and policy, and leave other model properties empty if you don’t need them to render UI.
You may also use “Just In Time” approaches to materialize additional data when needed, such as the Lazy
class, IQueryable,
or Task<T>
to execute expensive operations at invocation time. In the sample code, I leaned on Lazy
to show that messages are only logged by the request when the item is rendered to the view.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace TopSecretApp.Pages;
public class IndexModel(ILogger<IndexModel> logger) : PageModel
{
public Lazy<string> Message { get; } = new(() =>
{
logger.LogInformation("we reached the user message");
return "I like cats!";
});
public Lazy<string> AdminMessage { get; } = new(() =>
{
logger.LogInformation("we reached the admin message");
return "I also like dogs!";
});
}
ASP.NET Core will only invoke the two messages in my PageModel
if the sections using these properties render onto the page. In this example, we could exclude up to two expensive operations for anonymous users, leading to a better overall user experience.
Optimizing Policy Handlers
While we didn’t discuss it in this post, you may write custom policy handlers that rely on expensive resources such as a database. Be mindful that each policy evaluation will be executed by the custom policy handler and that, in such cases, every call will re-run the same database queries. Inefficient use of resources can lead to a degraded user experience.
Either load all information once at the start of a user request or use appropriate caching mechanisms to keep as much of the policy handler execution in memory as possible. Avoiding external resources is the best way to ensure a good performance profile, but that might not always be possible.
Finally, always measure the before and after states of policy additions and changes to ensure you haven’t introduced a performance issue.
Conclusion
You’ll need to make user interface decisions when working with your authentication and authorization systems. Since you will likely use policies across your ASP.NET Core solution, bringing them to your Razor Views makes sense.
With a single class, the AuthorizationPolicyTagHelper
, you can transform how you show and hide elements based on pre-existing authorization policies. Additionally, with a few considerations, you can improve the performance of your application by avoiding unnecessary code execution. It’s all so lovely and helpful.
I hope you enjoyed this post. If you have any questions, please let us know. You can also start a discussion in our Duende discussion forum through the comments below.