In part 3 of this series, I showed how to add role based authorization to a client-side Blazor application. In this post, I’m going to show you how to configure the newer, and recommended, policy-based authorization with Blazor.

All the code for this post is available on GitHub.

Introduction to Policy-based Authorization

Introduced with ASP.NET Core, policy-based authorization allows a much more expressive way of creating authorization rules. The policy model is comprised of three concepts:

  • Policy - Made up of one or more requirements.
  • Requirement - Collection of data parameters which are used by the policy to evaluate the current user principal.
  • Handler - Evaluates the requirements properties to decide if the current user principal has access to the requested resource.

Policies are most commonly registered at application startup in the Startup classes ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(config =>
    {
        config.AddPolicy("IsDeveloper", policy => policy.RequireClaim("IsDeveloper", "true"));
    });
}

In the example above, the policy IsDeveloper requires that a user have the claim IsDeveloper with a value of true.

Just as with roles you can apply policies via the Authorize attribute.

[Route("api/[controller]")]
[ApiController]
public class SystemController 
{
    [Authorize(Policy = “IsDeveloper”)]
    public IActionResult LoadDebugInfo()
    {
        // ...
    }
}

Blazors directives and components also work with policies.

@page "/debug"
@attribute [Authorize(Policy = "IsDeveloper")]
<AuthorizeView Policy="IsDeveloper">
    <p>You can only see this if you satisfy the IsDeveloper policy.</p>
</AuthorizeView>

Easier Management

The big advantage of policy-based authorization is the improvement to managing authorization within an application. With role-based auth, if we had a couple of roles which were allowed access to protected resources - let’s say admin and moderator. We would need to go to every area they were permitted access and add an Authorize attribute.

[Authorize(Roles = "admin,moderator")]

This doesn’t seem too bad initially, but what if a new requirement comes in and a third role, superuser, needs the same access? We now need to go round every area and update all of the roles. With policy-based auth we can avoid this.

We can define a policy in a single place and then apply it once to all the resources which require it. Then when extra roles need to be added, we can just update the policy from the central point without the need to update the individual resources.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(config =>
    {
    config.AddPolicy("IsAdmin", policy => policy.RequireRole("admin", "moderator", "superuser"));
    });
}

[Authorize(Policy = "IsAdmin")]

Building Custom Requirements

Policies are very flexible, you can build requirements based on roles, claims or you can even create your own custom requirements. Let’s look at how we can create a custom requirement.

Normally custom requirements are used when you have complex logic. As mentioned above, we will need to define a requirement and a handler which we then tie together using a policy.

As an example, let’s create a requirement that checks if a users email address is using a company domain. We need to start by creating a requirement, this class needs to implement the IAuthorizationRequirement interface, which is just an empty marker interface.

public class CompanyDomainRequirement : IAuthorizationRequirement
{
    public string CompanyDomain { get; }

    public CompanyDomainRequirement(string companyDomain)
    {
        CompanyDomain = companyDomain;
    }
}

Next we need to create a handler for our requirement. This needs to inherit from AuthorizationHandler<T> where T is the requirement to be handled.

public class CompanyDomainHandler : AuthorizationHandler<CompanyDomainRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CompanyDomainRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            return Task.CompletedTask;
        }
        
        var emailAddress = context.User.FindFirst(c => c.Type == ClaimTypes.Email).Value;
        
        if (emailAddress.EndsWith(requirement.CompanyDomain))
        {
            return context.Succeed(requirement);
        }
        
        return Task.CompletedTask;
    }
}

In the code above, we check if an email claim is present. If it is, then we check if it ends with the domain specified in the requirement. If it does then we return a success, otherwise we just return.

We just need to wire up our requirement with a policy and register the CompanyDomainHandler with the dependency injection container.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(config =>
    {
        config.AddPolicy("IsCompanyUser", policy =>
            policy.Requirements.Add(new CompanyDomainRequirement("newco.com")));
    });

    services.AddSingleton<IAuthorizationHandler, CompanyDomainHandler>();
}

For more in-depth information on custom requirements I recommend checking out the official docs.

Using policies with Blazor

Now we have an understanding of what policies are, let’s look at how we can use them in an application.

We’re going to swap the client-side Blazor application from part 3 over to policy based authorization. As part of doing this we’re going to see another advantage of policy based authorization, which is the ability to define policies in a shared project and reference them on both the server and the client.

Creating shared policies

We’re going to start by creating the policies in the shared project. We need to install the Microsoft.AspNetCore.Authorization package from NuGet in order to do this.

Once that’s installed create a new class called Policies with the following code.

public static class Policies
{
    public const string IsAdmin = "IsAdmin";
    public const string IsUser = "IsUser";

    public static AuthorizationPolicy IsAdminPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
                                               .RequireRole("Admin")
                                               .Build();
    }

    public static AuthorizationPolicy IsUserPolicy()
    {
        return new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
                                               .RequireRole("User")
                                               .Build();
    }
}

We start by defining a couple of constants - IsAdmin and IsUser. We’ll use these in a bit when registering the policies. Then there are the two policies themselves, IsAdminPolicy and IsUserPolicy. Here we’re using the AuthorizationPolicyBuilder to define each policy, both require the user to be authenticated then be in either the Admin role or User role, depending on the policy.

Configuring the server

Now we have defined our policies we need to tell our server application to use them. We’ll start by registering the policies in ConfigureServices in the Startup class. Add the following code under the existing call to AddAuthentication.

services.AddAuthorization(config =>
{
    config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
    config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});

The code is pretty self explanatory, we’re registering each policy and using the constants we defined in the Policies class to declare their names, which saves using magic strings.

If we move over to the SampleDataController we can update the Authorize attribute to use the new IsAdmin policy instead of the old role.

[Authorize(Policy = Policies.IsAdmin)]
[Route("api/[controller]")]
public class SampleDataController : Controller

Again, we can use our name constant to avoid the magic strings.

Configuring the client

Our server is now using the new policies we defined, all that’s left to do is to swap over our Blazor client to use them as well.

As with the server we’ll start by registering the policies in ConfigureServices in the Startup class. We already have a call to AddAuthorizationCore so we just need to update it.

services.AddAuthorizationCore(config =>
{
    config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
    config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});

In Index.razor, update the AuthorizeView component to use policies - still avoiding the magic strings.

<AuthorizeView Policy="@Policies.IsUser">
    <p>You can only see this if you satisfy the IsUser policy.</p>
</AuthorizeView>

<AuthorizeView Policy="@Policies.IsAdmin">
    <p>You can only see this if you satisfy the IsAdmin policy.</p>
</AuthorizeView>

Finally, update FetchData.razors Authorize attribute.

@attribute [Authorize(Policy = Policies.IsAdmin)]

That’s it! Our application is now moved over to policy-based authorization. We now have a more flexible authorization system which can use roles, claims, custom policies or any mixture of the above.

Server-side Blazor

I’ve not specifically talked about server-side Blazor for the simple reason that what we’ve done above should translate into server-side Blazor without any issues. However, I have included a server-side example in the code sample which accompanies this post on GitHub.

Note: The server-side sample currently has a build failure caused by this issue.

Summary

In this post, we’ve looked at policy-based authorization in ASP.NET Core and Blazor. We’ve looked at some of the advantages of using policy-based authorization over the more legacy roles-based authorization. Then we migrated the application from part 3 from roles-based auth to policy-based auth.