Share via



October 2017

Volume 32 Number 10

[Cutting Edge]

Policy-Based Authorization in ASP.NET Core

By Dino Esposito | October 2017

Dino EspositoThe authorization layer of a software application ensures that the current user is allowed to access a given resource, perform a given operation or perform a given operation on a given resource. In ASP.NET Core there are two ways to set up an authorization layer. You can use roles or you can use policies. The former approach—role-based authorization—has been maintained from previous versions of the ASP.NET platform, while policy-based authorization is new to ASP.NET Core.

The Authorize Attribute

Roles have been used in ASP.NET applications since the early days. Technically speaking, a role is a plain string. Its value, however, is treated as meta information by the security layer (checked for presence in the IPrincipal object) and used by applications to map a set of permissions to a given authenticated user. In ASP.NET the logged user is identified by an IPrincipal object, and in ASP.NET Core the actual class is Claims Principal. This class exposes a collection of identities and each identity is represented by IIdentity objects, specifically Claims Identity objects. This means that any logged user comes with a list of claims, which are essentially statements about her status. Username and role are two common claims of users of ASP.NET Core applications. However, role presence depends on the backing identity store. For example, if you use social authentication, you're never going to see roles.

Authorization goes one step further than authentication. Authentication is about discovering the identity of a user, whereas authorization is about defining requirements for users to call into application endpoints. User roles are typically stored in the database and retrieved when the user credentials are validated, at which point role information is attached in some way to the user account. The IIdentity interface features an Is In Role method that must be implemented. The Claims Identity class does that by checking that the Role claim is available in the collection of claims resulting from the authentication process. In any case, when the user attempts to call into a secured controller method, her role should be available for check. If not, the user is denied the call to any secured methods.

The Authorize attribute is the declarative way to secure a controller or some of its methods:

[Authorize]
public class CustomerController : Controller
{
  ...
}

Specified without arguments, the attribute only checks that the user is authenticated. However, the attribute supports additional attributes such as Roles. The Roles property indicates that users in any of the listed roles would be granted access. To require multiple roles, you can apply the Authorize attribute multiple times, or write your own filter.

[Authorize(Roles="admin, system"]
public class BackofficeController : Controller
{
  ...
}

Optionally, the Authorize attribute can also accept one or more authentication schemes through the Active Authentication Schemes property.

[Authorize(Roles="admin, system", ActiveAuthenticationSchemes="Cookie"]
public class BackofficeController : Controller
{
  ...
}

The Active Authentication Schemes property is a comma-separated string listing the authentication middleware components that the authorization layer will trust in the current context. In other words, it states that access to the Backoffice Controller class is allowed only if the user is authenticated through the Cookies scheme and has any of the listed roles. As mentioned, string values passed to the Active Authentication Schemes property must match authentication middleware registered at the startup of the application.

Note that in ASP.NET 2.0, authentication middleware is replaced with a service that has multiple handlers. As a result, an authentication scheme is a label that selects a handler. For more information about authentication in ASP.NET Core, you might want to check my September 2017 column, "Cookies, Claims and Authentication in ASP.NET Core" (msdn.com/magazine/mt842501).

Authorization Filters

The information provided by the Authorize attribute is consumed by the system-provided authorization filter. Because it's responsible for checking if the user is able to perform the requested operation, this filter runs before any of the other ASP.NET Core filters. If the user isn't authorized, the filter short-circuits the pipeline and cancels the requests.

Custom authorization filters can be created, but most of the time you won't need to do that. In fact, it's preferable that you configure the existing authorization layer on which the default filter relies.

Roles, Permissions and Overrules

Roles are an easy way to group the users of an application based on what they can or cannot do. But they aren’t very expressive; at least, not enough to meet the needs of most modern appli­cations. For example, consider a relatively simple authorization architecture, serving regular users of the Web site and power users authorized to access the back office software and update content. A role-based authorization layer can be built around two roles—user and admin—defining which controllers and methods each group can access.

You run into problems when it comes to overrules subtle distinctions that describe what users can or cannot do within a given role. For example, you may have users who enjoy access to back office systems. But of those users, some are authorized only to edit customer data, some only to work on content, and some to both edit customer data and work on content (see Figure 1).

Hierarchy of Roles

Figure 1 Hierarchy of Roles

Roles are essentially flat concepts. How would you flatten out even a simple hierarchy like the one shown in Figure 1? You could create four different roles—User, Admin, CustomerAdmin and ContentsAdmin—but as soon as the number of overrules grows, the number of required roles increases significantly. Even a simple exercise like this one shows that roles may not be the most effective way to handle authorizations, except for simple scenarios and instances where backward compatibility is a priority. For everything else, there’s a different requirement. Enter policy-based authorization.

What's a Policy, Anyway?

In ASP.NET Core, the policy-based authorization framework is designed to decouple authorization and application logic. Simply put, a policy is an entity devised as a collection of requirements, which themselves are conditions that the current user must meet.

The simplest policy is that the user is authenticated, while a common requirement is that the user is associated with a given role. Another common requirement is for the user to have a particular claim or a particular claim with a particular value. In the most general terms, a requirement is an assertion about the user identity that attempts to access a method that holds true. You create a policy object using the following code:

var policy = new AuthorizationPolicyBuilder()
  .AddAuthenticationSchemes("Cookie, Bearer")
  .RequireAuthenticatedUser()
  .RequireRole("Admin")
  .RequireClaim("editor", "contents")  .RequireClaim("level", "senior")
  .Build();

The builder object collects requirements using a variety of extension methods and then builds the policy instance. As you can see, requirements act on the authentication status and schemes, role, and any combination of claims read through the authentication cookie or bearer token.

If none of the predefined extension methods for defining requirements work for you, then you can always resort to defining a new requirement through your own assertion. Here's how:

var policy = new AuthorizationPolicyBuilder()
  .AddAuthenticationSchemes("Cookie, Bearer")
  .RequireAuthenticatedUser()
  .RequireRole("Admin")
  .RequireAssertion(ctx =>
  {
    return ctx.User.HasClaim("editor", "contents") ||
           ctx.User.HasClaim("level", "senior");
  })
  .Build();

The Require Assertion method takes a lambda that receives the Http Context object and returns a Boolean value. Therefore, the assertion is simply a conditional statement. Note that if you concatenate Require Role multiple times, then all roles must be honored by the user. If you want to express instead an OR condition, then you may resort to an assertion. In this example, in fact, the policy allows users that are either editor of contents or senior users.

Registering Policies

It's not enough to define policies—you must also register them with the authorization middleware. You do this by adding the authorization middleware as a service in the Configure Services method of the startup class, like so:

services.AddAuthorization(options=>
{
  options.AddPolicy("ContentsEditor", policy =>
  {
    policy.AddAuthenticationSchemes("Cookie, Bearer");
    policy.RequireAuthenticatedUser();
    policy.RequireRole("Admin");
    policy.RequireClaim("editor", "contents");
  });
}

Each policy added to the middleware has a name, which is used to reference the policy within the Authorize attribute on the controller class:

[Authorize(Policy = "ContentsEditor")]
public IActionResult Save(Article article)
{
  // ...
}

The Authorize attribute allows you to set a policy declaratively, but policies can also be invoked programmatically right from an action method, as shown in Figure 2.

Figure 2 Checking Policies Programmatically

public class AdminController : Controller
{
  private IAuthorizationService _authorization;
  public AdminController(IAuthorizationService authorizationService)
  {
    _authorization = authorizationService;
  }

  public async Task<IActionResult> Save(Article article)
  {    var allowed = await _authorization.AuthorizeAsync(      User, "ContentsEditor"));
    if (!allowed)
      return new ForbiddenResult();
    
    // Proceed with the method implementation 
    ...
  }
}

If the programmatic check of permissions fails, you might want to return a Forbidden Result object. Another option is returning Challenge Result. In ASP.NET Core 1.x, returning a challenge tells the authorization middleware to return a 401 status code, or redirect the user to a login page, depending on configuration. The redirect won't happen in ASP.NET Core 2.0, however, and even in ASP.NET Core 1.x the challenge ends up in a Forbidden Result if the user is already logged in. In the end, the best approach is to return Forbidden Result if the permission check fails.

Note that you can even perform a programmatic check of the policies from within a Razor view, as shown in the code here:

@{ 
  var authorized = await Authorization.AuthorizeAsync(
    User, "ContentsEditor"))}
@if (!authorized)
{
  <div class="alert alert-error">
    You’re not authorized to access this page.
  </div>
}

For this code to work, however, you must first inject the dependency on the authorization service, as follows:

@inject IAuthorizationService Authorization

Using the authorization service in a view can help hide elements of the UI that shouldn't be within the reach of the current user given the current context. Keep in mind, though, that simply hiding options in the view is not enough. You always have to enforce policies in the controller, as well.

Custom Requirements

The stock requirements basically cover claims, authentication and provide a general-purpose mechanism for customization based on assertions, but you can create custom requirements, too. A policy requirement is made of two elements: a requirement class that holds just data, and an authorization handler that validates the data against the user. Custom requirements extend your ability to express specific policies. For example, let's say you want to extend the Contents Editor policy by adding the requirement that the user must have at least three years of experience. Here's how you would do that:

public class ExperienceRequirement : IAuthorizationRequirement
{
  public int Years { get; private set; }

  public ExperienceRequirement(int minimumYears)
  {
    Years = minimumYears;
  }
}

A requirement must have at least one authorization handler. A handler is of type Authorization Handler<T>, where T is the requirement type. Figure 3 illustrates a sample handler for the Experience Requirement type.

Figure 3 Sample Authorization Handler

public class ExperienceHandler : 
             AuthorizationHandler<ExperienceRequirement>
{
  protected override Task HandleRequirementAsync( 
    AuthorizationHandlerContext context, 
    ExperienceRequirement requirement)
  {
    // Save User object to access claims
    var user = context.User;    if (!user.HasClaim(c => c.Type == "EditorSince"))      return Task.CompletedTask;

    var since = user.FindFirst("EditorSince").Value.ToInt();
    if (since >= requirement.Years)
      context.Succeed(requirement);

    return Task.CompletedTask;
  }
}

The sample authorization handler reads the claims associated with the user and checks for a custom Editor Since claim. If that isn't found, the handler returns without success. Success is returned only if the claim exists and contains an integer value not less than the specified number of years.

The custom claim is expected to be a piece of information linked in some way to the user—for example, a column in the Users table—saved to the authentication cookie. However, once you hold a reference to the user, you can always find the username from the claims and run a query against any database or external service to get hold of the years of experience and use the information in the handler. (I'll admit that this example would be a bit more realistic had the Editor Since value held a Date Time, and calculated if a given number of years had passed since the user began as an Editor.)

An authorization handler calls the method Succeed, passing the current requirement to notify that the requirement has been successfully validated. If the requirement didn't pass, the handler doesn't need to do anything and can just return. However, if the handler wants to determine the failure of a requirement regardless of the fact that other handlers on the same requirement may succeed, it calls the method Fail on the authorization context object.

Here's how you add a custom requirement to the policy (keep in mind, as this is a custom requirement, you have no extension method; rather, you must proceed through the Requirements collection of the policy object):

services.AddAuthorization(options =>
{
  options.AddPolicy("AtLeast3Years",
    policy => policy
      .Requirements
      .Add(new ExperienceRequirement(3)));
});

In addition, you have to register the new handler with the DI system under the scope of the IAuthorization Handler type:

services.AddSingleton<IAuthorizationHandler, ExperienceHandler>();

As mentioned, a requirement can have multiple handlers. When multiple handlers are registered with the DI system for the same requirement for the authorization layer, it suffices that at least one succeeds.

Accessing the Current HTTP Context

In the implementation of the authorization handler, you might need to inspect request properties or route data, like this:

if (context.Resource is AuthorizationFilterContext mvc)
{
  var url = mvc.HttpContext.Request.GetDisplayUrl();  ...
}

In ASP.NET Core, the Authorization Handler Context object exposes a Resource property set to the filter context object. The context object is different depending on the framework involved. For example, MVC and SignalR send their own specific object. Whether you cast depends on what you need to access. For example, the User information is always there, so you don't need to cast for that, but if you want MVC-specific details such as routing information, then you have to cast.

Wrapping Up

In ASP.NET Core authorization comes in two flavors. One is traditional role-based authorization, which works the same way it does in classic ASP.NET MVC, and still has the structural limitation of being rather flat and not ideal for expressing sophisticated authorization logic. Policy-based authentication is a new approach that provides a richer and more expressive model. This is because a policy is a collection of requirements based on claims and custom logic based on any other information that can be injected from the HTTP context or external sources. These requirements are each associated with one or more handlers, which are responsible for the actual evaluation of the requirement.


Dino Esposito is the author of “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) and “Modern Web Applications with ASP.NET” (Microsoft Press, 2016). A technical evangelist for the .NET and Android platforms at JetBrains, and frequent speaker at industry events worldwide, Esposito shares his vision of software at software2cents@wordpress.com and on Twitter: @despos.

Thanks to the following technical experts for reviewing this article: Barry Dorrans (Microsoft) and Steve Smith


Discuss this article in the MSDN Magazine forum