Practical ASP.NET

Declarative Claims-Based Authentication in ASP.NET Core 3.0

Eric Vogel's articles on authentication (here and here) in ASP.NET Core show what you have to do in order to authenticate a user against a local database. At the end of that process, you're ready to authorize the user based on information in the ClaimsPrincipal object created during the authentication process. Primarily, that means working with the claims assigned to that ClaimsPrincipal object. This post is about how to use ASP.NET Core's Authorize attribute (available in Controllers, Razor Pages, and Blazor components) to access the user's claims.

Caveat: I'm going to assume the authentication process that Eric has covered has been performed and also that your Startup class' Configure method contains, at the very least, these two lines of code:

app.UseAuthentication();
app.UseAuthorization();

Claims, Policies, and the Authorize Attribute
In ASP.NET Core, when you're using the Authorize attribute, you're always using claims. For example, as you have since the early days of ASP.NET, you can use the Authorize attribute to allow only users with a specific role to access a Controller. As Eric discussed at the end of his second article, this example would let only users in the Admin or ProductManager roles access the methods in the Product Controller:

[Authorize(Roles="Admin, ProductManager")]
public class ProductController : Controller
{

Similarly, with a Razor Page, you can use the Authorize attribute to control access to the Page's model class (but you can't control access to individual methods in a Razor Page):

[Authorize(Roles="Admin, ProductManager")]
public class ProductEditModel : PageModel
{

Because security is claims-based, what the Authorize attribute is actually doing is checking to see if the user (a) has one or more claims about the user's roles, and (b) that at least one of the claims has the value Admin or ProductManager.

What isn't obvious is that the Authorize attribute lets you work with any claim ... provided you first define a policy. A policy, in this context, is a "claim about a claim." In a policy you can just check that the ClaimsPrincipal object representing the current user has some specific claim -- for example, that the user has claimed to have an email address (you might want to check that claim before letting the user access your "Contact Us" web page). You can also, as the Authorize attribute does with roles, check that a claim is being made and that the claim has a specific value.

Creating Policies
You establish policies in ASP.NET's startup class in the ConfigureServices method. In that method, you first call the services collection's AddAuthorization method. That method, as is typical of services collection methods, accepts a lambda expression that allows you to configure the service (in this case, authorization). Your lambda expression is passed an AuthorizationOptions object that, in turn, has an AddPolicy method that allows you to create the policies you can use with your Authorize attribute.

The AddPolicy method accepts two parameters: A policy name and ... another lambda expression (stick with me here: I'm almost at the good stuff). The lambda expression is passed an AuthorizationPolicyBuilder parameter that has a RequireClaim method. It's that RequireClaim method that lets you specify a claim that must be present for the policy to be satisfied.

Typically, you'll use one of the predefined claims types to define a policy. This code, for example, creates a policy called MustHaveEmail that requires the user to have an email claim by using the Email claim type:

services.AddAuthorization(authopt =>
{
   authopt.AddPolicy("MustHaveEmail", polBuilder => polBuilder.RequireClaim(ClaimTypes.Email));
});

Now you can use that policy to control access to, in this case, a Controller:

[Authorize(Policy="MustHaveEmail")]
public class CommunicationController : Controller
{

You're not restricted to using the predefined claim types, however -- you can make up your own. This example defines a policy for a claim with the type HeadOffice:

authopt.AddPolicy("MustBeAssignedToHeadOffice", 
              polBuilder => polBuilder.RequireClaim("HeadOffice")); 

If the policy referenced in an Authorize attribute doesn't exist, then your application will throw an exception. If the policy does exist and the user doesn't meet the policy's requirements, then the user will be denied access. Only if the policy does exist and the user meets the requirements is the user allowed to use (in my previous examples) the methods in the Controller.

Often, of course, you want to specify more than that a user has a claim -- often you want to check that the user has specific value for that claim. You can do that by providing an array of values to the second parameter of the RequireClaim method.

This example establishes two policies with the second one (MustHaveASpecificName) requiring the user not only have a claim about their name but that the claim contain one of two specific values:

services.AddAuthorization(authopt =>
{
   authopt.AddPolicy("MustHaveEmail", 
         polBuilder => polBuilder.RequireClaim(ClaimTypes.Email));
   authopt.AddPolicy("MustHaveSpecificName", 
         polBuilder => polBuilder.RequireClaim(ClaimTypes.Name, 
                               new string[] { "Peter Vogel", "Jean Irvine"} ));
});

Combining Policies and Setting Defaults
You can stack up your Authorize attributes if you want. The following example requires the user to both be in the Guest role and have an email address in order to use the ContactUs method. However, in this example, the user doesn't have to satisfy the MustHaveEmail policy to use the Chat method:

[Authorize(Roles="Guest")]
public class CommunicationController : Controller
{
  [Authorize(Policy="MustHaveEmail")]
  public IActionResult ContactUs() { }

  public IActionResult Chat() { }

This example requires the user to be in the Guest role and to have an email address in order to access any method in the controller:

[Authorize(Policy="MustHaveEmail")]
[Authorize(Roles="Guest")]
public class EmailController : Controller
{

If you do have claims that are frequently used together, you can combine them into a single policy. You can do that in a fluent way by repeatedly calling the RequireClaim method on your policy builder class. This code, for example, creates a policy that requires users to have both a specific name and an email address:

authopt.AddPolicy("MustHaveSpecificNameAndEmail", polBuilder => 
          polBuilder.RequireClaim(ClaimTypes.Name, 
                        new string[] { "Peter Vogel", "Jean Irvine"})
                    .RequireClaim(ClaimTypes.Email));

If you are combining claims, it wouldn't be surprising if you wanted to include a claim about the user's role. There's a shortcut method called RequireRole that lets you build policies that include claims about the user's role. This policy, for example, requires a user to be in the Guest role and have an email address:

authopt.AddPolicy("MustHaveSpecificNameAndEmail", polBuilder => 
          polBuilder.RequireRole("Guest")
                    .RequireClaim(ClaimTypes.Email));

Finally, by setting the builder's DefaultPolicy property, you can provide a policy to be applied when the Authorize attribute is used without specifying roles or policies. To do set the default policy, you first create an AuthorizationPolicyBuilder object, then add some claims to it, and (finally) call the builder's Build method.

When an Authorize attribute with no role or policy specified, this code will require every user to have an email claim:

authopt.DefaultPolicy =
                new AuthorizationPolicyBuilder()
                .RequireClaim(ClaimTypes.Email)
                .Build();

Now, whenever you use the Authorize attribute without any parameters, your default policy will be applied. And, just to be clear, that means as soon as you specify any policies or roles in the Authorize attribute, this default policy will be ignored.

The Authorize attribute is a form of declarative security and, as a result, may not meet all your authorization needs. When, for example, you want to integrate authorization with your business logic, then you'll need more fine-grained control and will need to write some procedural code. But the ability to define the claims-based policies you need gives you a very flexible way to handle those coarse-grained authorization requirements. Who knows? You might never need to write any code at all.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube