Part 3: A better way to handle ASP.NET Core authorization – six months on

Last Updated: July 9, 2019 | Created: July 2, 2019

About six months ago I wrote the article “A better way to handle authorization in ASP.NET Core” which quickly became the top article on my web site, with lots of comments and questions. I also gave a talk at the NDC Oslo 2019 conference on the same topic which provided even more questions.

In response to all the questions I have developed a new, improved example application, called PermissionAccessControl2 (referred to as “version 2”) with series of new articles that cover the changes. The version 2 articles which cover improvements/changes based on questions and feedback.

Original articles:

NOTE: Some people had problems using the code in the original web application (referred to a version 1) in their applications, mainly because of the use of in-memory databases. The version 2 example code is more robust and supports real databases (e.g. SQL Server).

  • This article answers comments/questions raised by the first version and covers the following changes from the original feature authorization article. If you are new to this topic the original article is a better place to start as it explains the whole system.
  • The changed parts covered in this article are:
    • Simpler claims setup: Since the first article I have found a much simpler way to setup the users claims on login if you don’t need a refresh of the users claims.
    • Roles: In the original article I used ASP.NET Core’s identity Roles feature, but that adds some limitations. The version 2 example app has its own UserToRole class.
    • Using Permissions in the front-end: I cover how to use Permissions in Razor pages and how send the permissions to a JavaScript type front-end such as AngularJS, ReactJS etc.
    • Provides a SuperUser: Typically, a web app needs a user with super-high privileges to allow them set up a new system. I have added that to the new example app.
  • The version 2 example app is a super-simple SaaS (Software as a Service) application which provides stock and sales management to companies with multiple retail outlets – see this section.

Setting the scene – my feature/data authorization approach

As I explained in the first article in this series, I was asked to build a web application that provided a service to various companies that worked in the retail sector. The rules about what the users could access were quite complex and required me to go beyond the out-of-box features in ASP.NET Core. Also, the data was multi-tenant and hierarchical, i.e. each company’s data must be secured such that a user the data they are allowed to access.

Many (but not all) of the issues I solved for my client are generally useful so, with the permission of my client, I worked on an open-source example application to capture the parts of the authentication and authorization features that I feel are generally useful to other developers.

If you not familiar with ASP.NET’s authorization and authentication features, then I suggest you read the ”Setting the Scene” section in the first article.

Below is a figure showing the various parts/stages in my approach to feature/data authorization. I use the ASP.NET Core authentication part, but I replace the authorization stage with my own code.

My replacement authorization stage provides a few extra features over ASP.NET Core Role-based authorization.

  • I use Roles to represent the type of user that is accessing the system. Typical Roles in a retail system might be SalesAssistant, Manager, Director, with other specific-job Roles like FirstAider, KeyHolder.
  • In the ASP.NET Core code I use something I call Permissions, which represent a specific “use case”, such as CanProcessSale, CanAuthorizeRefund. I place a single Permission on each ASP.NET Core pages/web APIs I want to protect. For instance, I would add CanProcessSale Permission to all the ASP.NET Core pages/web APIs than are used in the process sale use case.
  • The DataKey is more complex because of the hierarchical data and I cover all the bases on how to be sure that this multi-tenant system is secure – see part 4 article.

There are other parts that I am not going to cover in this article as they are covered in other articles in this series. They are

  • Why using Enums to implement Permissions was a good idea (see this section in first article).
  • How I managed paid-for features (see this section in the first article).
  • How I setup and use a DataKey for segregate the data – see the part 4 article.

What the rest of this article does is deal with improvements I have made in the version 2 example application.

A simpler way to add to the User’s Claims

My approach relies on me adding some claims to the User, which is of type ClaimsPrincipal. In the original article I did this by using an Event in ApplicationCookie, mainly because that is what I had to use from my client. While that works I have found a much simpler way that adds the claims on login. This approach is much easier to write, works for Cookies and Tokens, and is more efficient. Thanks to https://korzh.com/blogs/net-tricks/aspnet-identity-store-user-data-in-claims for writing about this feature.

We do this by implementing a UserClaimsPrincipalFactory and registering it as a service. Here is my implementation of the UserClaimsPrincipalFactory.

public class AddPermissionsToUserClaims : 
UserClaimsPrincipalFactory<IdentityUser>
{
    private readonly ExtraAuthorizeDbContext _extraAuthDbContext;

    public AddPermissionsToUserClaims(UserManager<IdentityUser> userManager, 
        IOptions<IdentityOptions> optionsAccessor,
        ExtraAuthorizeDbContext extraAuthDbContext)
        : base(userManager, optionsAccessor)
    {
        _extraAuthDbContext = extraAuthDbContext;
    }

    protected override async Task<ClaimsIdentity> 
        GenerateClaimsAsync(IdentityUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        var userId = identity.Claims
           .SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier).Value;
        var rtoPCalcer = new CalcAllowedPermissions(_extraAuthDbContext);
        identity.AddClaim(new Claim(
            PermissionConstants.PackedPermissionClaimType,
            await rtoPCalcer.CalcPermissionsForUser(userId)));
        var dataKeyCalcer = new CalcDataKey(_extraAuthDbContext);
        identity.AddClaim(new Claim(
            DataAuthConstants.HierarchicalKeyClaimName, 
            dataKeyCalcer.CalcDataKeyForUser(userId)));
        return identity;
    }
}

You can see on line 17 I get the original claims by calling the base GenerateClaimsAsync. I then use the UserId to calculate the Permissions and the DataKey, which I add to the original claims. After this method has finished the rest of the login code will build the Cookie or Token for the user.

To make this work you need to register in the Configure method inside the Startup code using the following code:

services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, AddPermissionsToUserClaims>();

NOTE: The original code and this approach means that the claims are fixed until the user logs out and logs back in again. In the Part 5 article (coming soon!) I show ways to refresh the user’s claims when the roles/permissions have been changed by an admin person.

Adding our own UserToRole class

In the version 1 example app I used ASP.NET Core’s Role/RoleManger for a quick setup. But in a real application I wouldn’t do that as, I only want ASP.NET Core’s Identity system to deal with the authentication part.

The main reason for providing my own user to role class is because you can then do away with ASP.NET Core’s built-in identity database if you are using something like the OAuth 2 API (which my client system used). Also, if you are splitting the authorization part from the authentication part, then it makes sense to have the User-to-Roles links with all the other classes in the authentication part.

Note: Thanks to Olivier Oswald for his comments where he asked why I used the ASP.NET Core’s Role system in the first version. I was just being a bit lazy, so in version 2 I have done it properly.

In the version 2 example app I have my own UserToRole class, as shown below.

public class UserToRole
 {
     private UserToRole() { } //needed by EF Core

     public UserToRole(string userId, RoleToPermissions role) 
     {
         UserId = userId;
         Role = role;
     }
 
     public string UserId { get; private set; }
     public string RoleName { get; private set; }

     [ForeignKey(nameof(RoleName))]
     public RoleToPermissions Role { get; private set; }
    
     //… other code left out
}

The UserToRole class has a primary key which is formed from both the UserId and the RoleName (known as a “composite key”). I do this to stop duplicate entries linking a User to a Role, which would make managing the User’s Role more difficult.

I also have a method inside the UserToRole class called AddRoleToUser. This adds a Role to a User with check as adding a duplicate Role to a User will cause a database exception, so I catch that early and sent a user-friendly error message to the user.

Using Permissions in the front-end

The authorization side of ASP.NET returns HTTP 403 (forbidden) if a user isn’t allowed access to that method. But to make a better experience for a user we typically want to remove any links, buttons etc. that the user isn’t allowed to access. So how do I do that with my Permissions approach?

Here are the two ways you might be implementing your front-end, and how to handle each.

1. When using Razor syntax

If you are using ASP.NET Core in MVC mode or Razor Page mode, then you can use my extension method called  UserHasThisPermission. The code below comes from the version 2 _layout.cshtml file and controls whether the Shop menu appears, and what sub-menu items appear.

@if (User.UserHasThisPermission(Permissions.SalesRead))
{
    <li class="nav-item">
        <div class="dropdown">
            <a class="nav-link text-dark dropdown-toggle" role="button" 
                id="dropdown1MenuButton" data-toggle="dropdown" 
                aria-haspopup="true" aria-expanded="false">
                Shop
            </a>       
            
            <div class="dropdown-menu" aria-labelledby="dropdown1MenuButton">
                @if (User.UserHasThisPermission(Permissions.SalesSell))
                {
                    <a class="nav-link text-dark" asp-area="" asp-controller="Shop" asp-action="Till">Till</a>
                }
                <a class="nav-link text-dark" asp-area="" asp-controller="Shop" asp-action="Stock">Stock</a>
                @if (User.UserHasThisPermission(Permissions.SalesRead))
                {
                    <a class="nav-link text-dark" asp-area="" asp-controller="Shop" asp-action="Sales">Sales</a>
                }
            </div>
        </div>
    </li>
}

You can see me using the UserHasThisPermission method on lines 1, 12 and 17.

2. Working with a JavaScript front-end framework

In some web  applications you might use a JavaScript front-end framework such as AngularJS, ReactJS etc. to manage the front-end. In that case you need to pass the Permissions to your front-end system so that you can add code to control what link/buttons are shown to the user.

It is very simple to get access to the current user’s Permissions via the HttpContext.User variable which is available in any controller. I do this via a Web API and here is the code from my FrontEndController in my version 2 application.

[HttpGet]
public IEnumerable<string> Get()
{
    var packedPermissions = HttpContext.User?.Claims.SingleOrDefault(
        x => x.Type == PermissionConstants.PackedPermissionClaimType);
    return packedPermissions?.Value
        .UnpackPermissionsFromString()
        .Select(x => x.ToString());
}  

This action returns an array of Permission names. Typically you would call this after a login and store the data in a SessionStorage for use while the user is logged in. You can try this in the version 2 application – run the application locally and go to http://localhost:65480/swagger/index.html. Swagger will then display the FrontEnd API which has one command to get the user’s permissions.

NOTE: In part 5, where the permissions can change dynamically I show a way that the front-end can detect that the permissions have changed so they can update their local version of the Permissions.

Enabling a SuperAdmin User

In most web application I have built you need one user that has access to every part of the system – I call this user SuperAdmin. And typically, I have some code that will make sure there is a SuperAdmin user in any system that the application runs. That way you can run the app with a new database and then use the SuperAdmin user to set up all the other users you need.

A logged in SuperAdmin User needs all the Permissions so that they can do anything, but that would be hard to keep updated as new permissions are added. Therefore, I added a special Permission called AccessAll and altered the UserHasThisPermission extension method to return true if the current user has the AccessAll Permission.

public static bool UserHasThisPermission(
    this Permissions[] usersPermissions, 
    Permissions permissionToCheck)
{
    return usersPermissions.Contains(permissionToCheck) 
        || usersPermissions.Contains(Permissions.AccessAll);
}

Now we have the concept of a SuperAdmin we need a way to create the SuperAdmin user. I do this via setup code in the Program class. This method makes sure that the SuperAdmin user is in the current user database, i.e. it adds a SuperUser if . Note: I am using the C# 7.1’s Main Async feature to run my startup code.

public static async Task Main(string[] args)
{
    (await BuildWebHostAsync(args)).Run();
}

public static async Task<IWebHost> BuildWebHostAsync(string[] args)
{
    var webHost = WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();

    //Because I might be using in-memory databases I need to make 
    //sure they are created before my startup code tries to use them
    SetupDatabases(webHost);

    await webHost.Services.CheckAddSuperAdminAsync();
    await webHost.Services.CheckSeedDataAndUserAsync();
    return webHost;
}

The CheckAddSuperAdminAsync obtains the SuperAdmin Email and password from the appsetting.json file (see Readme file for more information).

NOTE: The SuperAdmin user is very powerful and needs to be protected. Use a long, complicated password and make sure you provide the SuperAdmin email and password by overriding the appsettings.json values on deployment. My startup code also doesn’t allow a new SuperAdmin user to be added if there is a user already in the database that has the SuperAdmin role. That stops someone adding a new SuperAdmin with their own email address once a system is live.

Other things new in version 2 of the example application

The PermissionAccessControl2 application is designed to work like a real application, using both feature and data authorization. The application pretends to be a super-simple retail sales application service for multiple companies, i.e. a multi-tenant application. I cover the multi-tenant data authorization in the part 4 article, Building a robust and secure data authorization with EF Core.

The differences in version 2 of version 1 that I have not already mentioned are:

  • The code will work with either in-memory databases or SQL Server databases, i.e. it will check if users and data is already present and is so won’t try to add the user/data again
  • You can choose various application setups, such as database type and simple/complex claims setup. This is controlled by data in the appsettings.json file – see Readme file for more information on this.
  • The multi-tenant data access is hierarchical, with much more complex and robust than in version 1 – see Part 4: Building a robust and secure data authorization with EF Core for more on this.
  • There are some unit tests. Not a lot, but enough to give you an idea of what is happening.

Conclusion

The first article on my approach authorization in ASP.NET Core has been very popular, and I had great questions via my blog and at my NDC Oslo talk. This caused me to build a new version of the example app, available via a GitHub repo, with many improvements and some new articles that explains the changes/improvements.

In this article (known as Part 3) I focused on the ASP.NET Core authorization part and provided a few improvements over the version 1 application and added explanations of how to use these features.  In article Part 4 I cover data authorization with EF Core, and the Part 5 article covers the complex area of updating a user’s permissions/data key dynamically.

Hopefully the whole series, with the two example applications, will help you design and build your own authorization systems to suit your needs.

Happy coding!

 

If you have a ASP.NET Core or Entity Framework Core problem that you want help on then I am available as a freelance contractor. Please send me a contact request via my Contact page and we can talk some more on Skype.