Part 7 – Adding the “better ASP.NET Core authorization” code into your app

Last Updated: July 31, 2020 | Created: September 28, 2019

I have written a series about a better way to handle authorization in ASP.NET Core which add extra ASP.NET Core authorization features. Things like the ability to change role rules without having to edit and redeploy your web app, or adding the ability to impersonate a user without needing their password. This series has quickly become the top articles on my web site, with lots of comments and questions.

A lot of people like what I describe in these articles, but they have problems extracting the bits of code to implement the features this article describes. This article, with its improved PermissionAccessControl2 repo, is here to make it much easier to pick out the code you need and put it into your ASP.NET Core application. I then give you a step by step example of selecting a “better authorization” feature and putting it into a ASP.NET Core 2.0 app.

I hope this will help people who like the features I describe but found the code really hard to understand, and here is a list of all the articles in the series so that you can find information on each feature.

TL;DR; – summary

  • The “better authorisation” series provides a number of extra features to ASP.NET Core’s default Role-based authorisation system.
  • I have done a major refactor to the companion PermissionAccessControl2 repo on GitHub to make it easier for someone to copy the code they need for the features they want to put in their code.
  • I have also built separate authorization methods for the different combinations of my authorization features. This means it easier for you to pick the feature that works for you.
  • I have also improved/simplified some of the code to make it easier to understand.
  • I then use a step by step description of what you need to do to add a “better authorization” feature/code into your ASP.NET Core application.
  • I have made a copy of the code I produced in these steps available via the PermissionsOnlyApp repo in GitHub.

Setting the scene – the new structure of the code

Over the six articles I have added new features on top of the features that I already implemented. This meant the final version in the PermissionAccessControl2 repo was really complex, which made  it hard work for people to pick out the simple version that they needed. In this big refactor I have split the code into separate projects so that its easy to see what each part of the application does. The diagram below shows the updated application projects and how they link to each other.

This project may look complex, but many of these projects contain less than ten classes. These projects allow you to copy classes etc. from the projects that cover the features you want, and ignore projects that cover features you don’t want.

The other problem area was the database. Again, I wanted the classes relating to the added Authorization code kept separate from the classes I used to provide a simple, multi-tenant example. This let me to have a non-standard database that was hard to create/migrate. The good news is you only need to copy over parts of the ExtraAuthorizeDbContext DbContext into your own DbContext, as you will see in the next sections.

The Seven versions of the features

Before I get into the step by step guide, I wanted to list the seven different classes that provide different mixes of features that you can now easily pick and choose.

In fact in the PermissionAccessControl2 application allows you to configure different parts of the application so that you can try different features without editing any code. The feature selection is done by the “DemoSetup” section in the appsettings.json file. The AddClaimsToCookies class reads the “AuthVersion” value on startup to select what code to use to set up the authorization. Here are the seven classes that can be used, with their “AuthVersion” value.

  1. Applied when you log in (via IUserClaimsPrincipalFactory>
    1. LoginPermissions: AddPermissionsToUserClaims class.
    1. LoginPermissionsDataKey: AddPermissionsDataKeyToUserClaims class.
  2. Applied via Cookie event
    1. PermissionsOnly: AuthCookieValidatePermissionsOnly class
    1. PermissionsDataKey: AuthCookieValidatePermissionsDataKey class
    1. RefreshClaims : AuthCookieValidateRefreshClaims class
    1. Impersonation : AuthCookieValidateImpersonation class
    1. Everything : AuthCookieValidateEverything class

Having all these version makes it easier for you to select the feature set that you need for your specific application.

Step-by-Step addition to your ASP.NET Core app

I am now going to take you through the steps of copying over the most useful asked for in my “better authorization” series – that is the Roles-to-Permissions system which I describe here in the first article. This gives you two features that aren’t in ASP.NET Core’s Roles-based authorization, that is:

  • You can change the authorization rules in your application without needing to edit code and re-deploy your application.
  • You have simpler authorization code in your application (see section “Role authorization and its limitations” for more on this).

I am also going to use the more efficient UserClaimsPrincipalFactory method (see this section of article 3 for more on this) of adding the Permissions to the user’s claims. This add the correct Permissions to the user’s claims when thye log in.

And because .NET Core 3 is now out I’m going to show you how to do this for a ASP.NET Core 3.0 MVC application.

NOTE: I have great respect for Microsoft’s documentation, which has become outstanding with the advent of NET Core. The amount of information and updates on NET Core was especially good – fantastic job everyone.  I also have come to love Microsoft’s step-by-step format which is why I have tried to do the same in this article. I don’t think I made it as good as Microsoft, but I hope it helps someone.

Step 1: Is your application ready to take the code?

Firstly, you must have some form of authorisation system, i.e. something that checks the user is allowed to login. It can be any for of authorization system (my original client used Auth0 with the OAuth2 setup in ASP.NET Core). The code in my example PermissionAccessControl2 repo has some approaches that work with most authorisation, but some of the more complex ones only work with system that store claims in a cookie (although the approach could be altered to tokens etc.).

In my example I’m going to go with a ASP.NET Core 3.0 MVC app with Authentication set to “Individual User Accounts->Store user accounts in-app”. This means there is database added to your application and be default it uses cookies to hold the logged-in user’s Claims. Your system may be different, but with this refactor its easier for you to work out what code you need to copy over.

Step 2:  What code will we need for PermissionAccessControl2 repo?

You need to start out by deciding what parts of the PermissionAccessControl2 projects you need. This will depend on what features you need. Because the projects having names that fit the features this makes it easier to find things.

In my example I’m only going to use the core Roles-to-Permissions feature so I only need the code in the following projects:

  • AuthorizeSetup: just the AddPermissionsToUserClaims class
  • FeatureAuthorize: All the code.
  • DataLayer: A cut-down the ExtraAuthorizeDbContext DbContext (no DataKey, no IAuthChange) and some, but not all, of the classes in the ExtraAuthClasses folder.
  • PermissionParts: All the code.

That means I only need to look at about 50% of the projects in the PermissionAccessControl2 repo.

Step 3: Where should I put the PermissionAccessControl2 code in my app?

This is an architectural/style decision and there aren’t any firm rules here. I lean towards separating my apps into layers (see the diagram in this section of an article on business logic). There are lots of ways I could do it, but I went for a simple design as shown below.

Step 4: Moving the PermissionsParts into DataLayer

That was straightforward – no edits needed and no NuGet packages needed to be installed.

Step 5: Moving the ExtraAuthClasses into DataLayer

The ExtraAuthClasses contain code for all features, which makes this part the most complex step as you need to remove parts that aren’t used by features you want to use. Here is a list of what I needed to do.

5.a Remove unwanted ExtraAuthClasses.

I start by removing and ExtraAuthClasses that I don’t need because I’m not using those features. In my example this means I delete the following classes

  • TimeStore – only needed for “refresh claims” feature.
  • UserDataAccess, UserDataAccessBase and UserDataHierarchical – these are about the Data Authorize feature, which we don’t want.

5.b Fix errors

Because I didn’t copy over projects/code for features I don’t want then some things showed up as compile errors, and some just need to be changed/deleted.

  • Make sure the Microsoft.EntityFrameworkCore NuGet package has been added to the DataLayer.
  • You will also need the Microsoft.EntityFrameworkCore.Relational NuGet package in DataLayer for some of the configuration.
  • Remove the IChangeEffectsUser and IAddRemoveEffectsUser interfaces from classes – they are for the “refresh claims” feature.
  • Remove any using statements that don’t link to left out/moved projects.

NOTE: I used some classes/interfaces from my EfCore.GenericServices library to handle error handling in some of the methods in my ExtraAuthClasses. But I am building this app three days after the release of NET Core 3.0 and I haven’t (yet) updated GenericServices to NET Core 3.0 so I added a project called GenericServicesStandIn to host the Status classes.

5.b Adding ExtraAuthClasses to your DbContext

I assume you will have a DbContext that you will use to access the database. Your application DbContext might be quite complex, but in my example PermissionOnlyApp I started with a basic application DbContext as shown below.

public class MyDbContext : DbContext
{
    //your DbSet<T> properties go here

    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //Your configuration code will go here
    }
}

Then I needed to add the DbSet<T> for the ExtraAuthClasses I need, plus some extra configuration too.

public class MyDbContext : DbContext
{
    //your DbSet<T> properties go here

    //Add the ExtraAuthClasses needed for the feature you have selected
    public DbSet<UserToRole> UserToRoles { get; set; }
    public DbSet<RoleToPermissions> RolesToPermissions { get; set; }
    public DbSet<ModulesForUser> ModulesForUsers { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //Your configuration code will go here

        //ExtraAuthClasses extra config
        modelBuilder.Entity<UserToRole>().HasKey(x => new { x.UserId, x.RoleName });
        modelBuilder.Entity<RoleToPermissions>()
            .Property("_permissionsInRole")
            .HasColumnName("PermissionsInRole");
    }
}

Now you can replace some of the references to ExtraAuthorizeDbContext references in some of the ExtraAuthClasses.

Step 6: Move the code into the RolesToPermissions project

Now we need to move the last of the Roles-to-Permissions code into the RolesToPermissions project. Here are the steps I took.

6a. Move code into this project from the PermissionAccessControl2 version.

What you move in depends on what features you want. If you have some of the complex feature like refresh user’s claims on auth changes, or user impersonation you might want to put each part in a separate folder. But in my example, I only need code from one project and pick one class from another. Here is what I did.

  • I moved all the code in the FeatureAuthorize project (including the code in the PolicyCode folder).
  • I then copied over the specific setup code I needed for the feature I wanted I this case this was the AddPermissionsToUserClaims class.

At this point there will be lots of errors, but before we sort these out you have to select the specific setup code you need.

6b. Add project and NuGet packages needed by the code

This code accessing a number of support classes for it to work. The error messages on “usings” on in the code will show you what is missing. The most obvious is this project needs a reference to the DataLayer. Then there are NuGet packages – I only needed three, but you might need more.

6c. Fix references and “usings”

At this point I still have compile errors, either because the applciation’s DbContext is a different name, and because it has some old (bad) “usings”. The steps are:

  • The code refers to ExtraAuthorizeDbContext, but now it needs to reference your application DbContext. In my example that is called MyDbContext. You will also need to add a “using DataLayer” to reference that.
  • You will have a number incorrect “using” at the top of various files – just delete them.

Step 7 – setting up the ASP.NET Core app

We have added all the Roles-to-Permission code we need, but the ASP.NET Core code doesn’t use it yet. In this section I will add code to the ASP.NET Core Startup class to link the   AddPermissionsToUserClaims into the UserClaimsPrincipalFactory. Also I assume you have a database your already use to which I have add the ExtraAuthClasses. Here is the code that does all this

public void ConfigureServices(IServiceCollection services)
{
    // … other standard service registration removed for clarity

    //This registers your database, which now includes the ExtraAuthClasses
    services.AddDbContext<MyDbContext>(options =>
        options.UseSqlServer(
              Configuration.GetConnectionString("DemoDatabaseConnection")));

    //This registers the code to add the Permissions on login
    services.AddScoped<
         IUserClaimsPrincipalFactory<IdentityUser>, 
         AddPermissionsToUserClaims>();

    //Register the Permission policy handlers
    services.AddSingleton<IAuthorizationPolicyProvider,
         AuthorizationPolicyProvider>();
    services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
}

NOTE: In my example app I also set the options.SignIn.RequireConfirmedAccount to false in the AddDefaultIdentity method that registers ASP.NET Core Indentity. This allow me to log in via a user I add at startup (see next section). This demo user won’t have its email verified so I need to turn off that constraint. In a real system you might not need that.

At this point all the code is linked up, but we need an EF Core migration to add the ExtraAuthClasses to your application DbContext. Here is the command I run in Visual Studio’s Package Manager Console – note that it has to have extra parameters because there are two DbContexts (the other one is the ASP.NET Idenity ApplicationDbContext).

Add-Migration AddExtraAuthClasses -Context MyDbContext -Project
DataLayer

Step 8 – Add Permissions to your Controller actions/Razor Pages

All the code is in place so now you can use Permissions to protect your ASP.NET Core Controller actions or Razor Pages. I explain Roles-to-Permissions concept in detail in the first article, so I’m not going to cover that again. I’m just going to show you how a) to add a Permission to the Permissions Enum and then b) protect an ASP.NET Core Controller action with that Permission.

8a. Edit the Permissions.cs file

In the code you copied into the PermissionsParts folder in the DataLayer you will find a file called Permissions.cs file, which defines the enum called “Permissions”. It has some example Permissions which you can remove, apart for the one named “AccessAll” (that is used by the SuperAdmin user). Then you can add the Permission names you want to use in your application. I typically I give each Permission a number, but you don’t have to – the compiler will do that for you. Here is my updated Permissions Enum.

public enum Permissions : short //I set this to short because the PermissionsPacker stores them as Unicode chars 
{
    NotSet = 0, //error condition

    [Display(GroupName = "Demo", Name = "Demo", Description = "Demo of using a Permission")]
    DemoPermission = 10,

    //This is a special Permission used by the SuperAdmin user. 
    //A user has this permission can access any other permission.
    [Display(GroupName = "SuperAdmin", Name = "AccessAll", Description = "This allows the user to access every feature")]
    AccessAll = Int16.MaxValue, 
}

8b. Add HasPermission Attribute to a Controller action

To protect an ASP.NET Core Controller action or Razor Page you use the “HasPermission” attribute to them. Here is an example from the PermissionOnlyApp.

public class DemoController : Controller
{
    [HasPermission(Permissions.DemoPermission)]
    public IActionResult Index()
    {
        return View();
    }
}

For this action to be executed the caller must a) by logged in and b) either have the Permission “DemoPermission” in their set of Permissions, or be the SuperAdmin user who has the special Permission called “AccessAll”.

At this point you are good to go apart from creating the databases.

Step 9 – extra steps to make it easier for people to try the application

I could stop there, but I like to make sure someone can easily run the application to try it out. I therefore am going to add:

  • Some code in Program to automatically migrate both databases on startup (NOTE: This is NOT the best way to do migrations as it fails in certain circumstances – see my articles on EF Core database migrations).
  • I’m going to add a SuperAdmin user to the system on startup if they aren’t there. That way you always have a admin user available to log in on startup – see this section from the part 3 article about the SuperAdmin and what that user can do.

I’m not going to detail this because you will have your own way of setting up your application, but it does mean you can just run this application from VS2019 and it should work OK (it does use the SQL localdb).

NOTE: The first time you start the application it will take a VERY long time to start up (> 25 seconds on my system). This is because it is applying migrations to two databases. The second time will be much faster.

Conclusion

Quite a few people have contacted me with questions about how they can add the features I described in these series into their code. I’m sorry it was so complex and I hope this new article. I also took the time to improve/simplify some of the code. Hopefully this will make it easier to understand and transfer the ideas and code that goes with this these articles.

All the best with your ASP.NET Core applications!

4.6 5 votes
Article Rating
Subscribe
Notify of
guest
59 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Rishi Pujara
1 year ago

Thanks a lot for wonderful article. I was wondering how can we implement requirements like where in hierarchy user can access selected children data rather than all children’s’ data. For example a user created at Sub group which has 3 retail outlets and we want to restrict user to access data of only 2 Retail outlets out of 3.

Rishi Pujara
1 year ago
Reply to  Jon P Smith

Hi Jon Thanks for reply. I was wondering if like data key which is very powerful in terms of performance as well, we can implement. I understood you point, i have to further filter data using where clause and one to many mapping (User-Tenants)

Nileksh
Nileksh
1 year ago

This line not working for me.

//This registers the code to add the Permissions on login
services.AddTransient<IUserClaimsPrincipalFactory<IdentityUser>, AddPermissionsToUserClaims>();

Can any one help me for this?

I got this error:

Some services are not able to be constructed (Error while validating the service descriptor 
‘ServiceType: Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory1[Microsoft.AspNetCore.Identity.IdentityUser] 
Lifetime: Scoped ImplementationType: FXV_App.AuthorizeSetup.AddPermissionsToUserClaims': 
Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager
1[Microsoft.AspNetCore.Identity.IdentityUser]’ 
while attempting to activate ‘My_App.AuthorizeSetup.AddPermissionsToUserClaims’.)

InvalidOperationException: Unable to resolve service for type ‘Microsoft.AspNetCore.Identity.UserManager`1[Microsoft.AspNetCore.Identity.IdentityUser]’ 
while attempting to activate ‘My_App.AuthorizeSetup.AddPermissionsToUserClaims’.

Nileksh
Nileksh
1 year ago
Reply to  Jon P Smith

Hi thanks for the reply I fixed that issue. Now I have another issue. When I logged in it shows 403 error when I permission attribute to my controller action.
After user found I used JWT mechanism to login and manage claims.

Here is the function:

 public async Task<string> GenerateToken(SymmetricSecurityKey key, int exp, AppUser user)
        {
            var signInKey = key;


            var role = await userManager.GetRolesAsync(user);


            //just in case of this token only for confirmaion email
            var ip = (exp == 0) ? "192.0.0.0" : accessor.HttpContext.Connection.RemoteIpAddress.ToString();


            if (role.Count == 0 && exp == 0)
            {
                role.Add("Athlete");
            }
            var token = new JwtSecurityToken(
                issuer: "localhost",
                audience: "localhost",
                claims: new[]
                {
                        new Claim ("Ip",ip),
                        new Claim ("FirstName",user.FirstName),
                        new Claim ("LastName",user.LastName),
                        new Claim ("Uid",user.Id.ToString()),
                        new Claim ("Role",role[0]),
                        new Claim ("Permission",user.P_ID.ToString()),
                        new Claim ("Name",user.Email),
                        new Claim ("Img Path",(user.Profile_Img_Path == null || user.Profile_Img_Path == "" || user.Profile_Img_Path == "") ? "/sources/userProfileImg/user-profile-null.png" : user.Profile_Img_Path)
                },
                expires: DateTime.UtcNow.AddMinutes(exp),
                signingCredentials: new SigningCredentials(signInKey, SecurityAlgorithms.HmacSha256)
                );


            return new JwtSecurityTokenHandler().WriteToken(token);
        }

I think claim add process is not working.

Can you please help me why this issue occur?

Saurabh
Saurabh
2 months ago
Reply to  Nileksh

I am getting same issue, can you please tell me how you resolve this?

Greg Zapp
Greg Zapp
2 years ago

Hi

Thanks for the great series.

I’m still making my way through the articles(on Part 2). I’m new to Asp.Net Core and eyeing it for a new OSS project as I’m a bit of a dotnet fanboy.

My needs for authorization will be more complex like yours and I think I can sum them up by saying I need to create and RBAC(ABAC?) based ACL system like AWS/GCP/Azure IAM or Kubernetes/OpenShift.

My experience researching this thus far has lead to some very mixed feelings about asp.net core auth. The interfaces seem to be missing basic stuff like looking up the requirements for a user/group/role/policy(for optimizing data access and other stuff in my case).

It seems like with all the additional work required just replacing the entire system would be a tiny fraction of the overall effort. I’m curious what I would be missing out on if I choose to create my own auth service and interfaces that more closely match my requirements?

Last edited 2 years ago by Greg Zapp
Simon
Simon
2 years ago

Hi

thank you for the great article.

I have one problem when trying to implement your solution.
I want to use everything up to the impersonation feature. I think I have implemented everything correct. The only problem is that now I cant logout.
I have traced the problem back to the startup code.

services.ConfigureApplicationCookie(options =>
{
    options.Events.OnValidatePrincipal = new AuthCookieValidateImpersonation().ValidateAsync;
    //This ensures the impersonation cookie is deleted when a user signs out
    options.Events.OnSigningOut = new AuthCookieSigningOut().SigningOutAsync;
    options.Cookie.Name = "Adibo";
    options.ExpireTimeSpan = TimeSpan.FromDays(30);
});

The Problem seems to be in the line

options.Events.OnValidatePrincipal = new AuthCookieValidateImpersonation().ValidateAsync;

If I have both lines in my code, it wont call the SignOutAsync Method specified here. If I comment the first line out, it will call it.
Both happens even if I dont tell it to impersoante someone.

Do you have an idea of what is happening here?

Simon
Simon
2 years ago
Reply to  Jon P Smith

The original code works just fine.
the name and timespan should only be changed in the setup not everytime the user logs out.

Might the issue be that i dont have setup permissions/roles yet?

Simon
Simon
2 years ago
Reply to  Simon

My problem most likely is my cookie auth setup. do you know a good article for that?

Simon
Simon
2 years ago
Reply to  Jon P Smith

I changed line 51 in AuthCookieValidateImpersonation.cs from

var identity = new ClaimsIdentity(newClaims, "Cookie");

To

var identity = new ClaimsIdentity(newClaims, "Identity.Application");

Is it possible that there is a version problem? Im using Microsoft.AspNetCore.Identity.Core Version 5.0.5

I tried everything i could find to set AuthenticationScheme to Cookie and nothing works

Last edited 2 years ago by Simon
Jonathan
Jonathan
2 years ago

Thank you for such an excellent series of articles, your hard work and sharing of the code.

A quick question….

For multi-tenant, am I correct in thinking, the overall objective is for data rows in newly added child tables implementing IShopLevelDatakey e.g. Stock, to be ‘owned’ by their parent TenantBase e.g. RetailOutlet rather than the user that created them? So, especially at create time, the child data row should be marked with the Datakey for the TenantBase/RetailOutet parent rather than the Datakey in the User’s Claims? All users with a hierarchial Datakey for that RetaiOutlet or above should then have data row visibility of the child rows?

The current version of MarkWithDataKeyIfNeeded uses the Datakey sourced from the user’s claim (via CompanyDbContext constructor) to set the DataKey on any IShopLevelDataKey rows during SaveChanges().  That coincidently works if the current user is tentant-linked to the parent RetailOutet but not if they are tenant-linked to a SubGroup or Company.  
My current thought is to use the controller/page action to set the new child row datakey via ‘SetShopLevelDataKey(parentRetailOutlet.DataKey)’ and modify MarkWithDataKeyIfNeeded() to only call SetShopLevelDataKey() if the DataKey is not already set…..

  public static void MarkWithDataKeyIfNeeded(this DbContext context, string accessKey)
    {
      //at startup access key can be null. The demo setup sets the DataKey directly.
      if (accessKey == null)
        return;

      foreach (var entityEntry in context.ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added))
      {
        if ( (entityEntry.Entity is IShopLevelDataKey shopItem) && (shopItem.DataKey == null))
          shopItem.SetShopLevelDataKey(accessKey);
      }
    }

Does this make sense or would you have a better suggestion?

Mohamed Aarab
Mohamed Aarab
2 years ago

I have an API and an Authentication server with Identity Server 4 installed, but I’m not entirely sure where to place some of these files. Because my login takes place on the auth server and deals out tokens and then there are the controller routes on the API that needs protecting, both projects need access to most of the classes and enums. For example Permissions.cs is needed in both the API and the Auth Server. How would you go on about this? Is it okay to have duplicate Permissions.cs?

Nina Andersen
Nina Andersen
2 years ago

This looks really great. Thanks for all your effort.

Where would I hook in a distributed cache, like redis so that I can run multiple instances of the UI behind a load balancer?

Vinh Nguyen
Vinh Nguyen
2 years ago

Hi Jon, in C#, we have enum flag feature to have enum as a combination of enum values. Why dont we use this to store the permissions linked with role in database instead of a string? I think we can also do that with permission claim in user’s claims.

Vinh Nguyen
Vinh Nguyen
2 years ago
Reply to  Jon P Smith

Thanks for you answer, Jon. Actually, i thought about the limitation of enum flag on my way home 😀

Vinh Nguyen
Vinh Nguyen
2 years ago

Hello, it is such a great series that is really help for me when searching for a authorization approach that can fit my project context. I have one question for you when reading your articles. You are using cookie authentication in your example as well as explaination. You can use OnValidatePrincipal to update the cookie if there is any case such as: out of date permissions, impersionation change… But if i use bearer authentication using JWT format for token, how can i alert client about new token that it needs to get for next request? I thought about a solution is that i will add a header value in the response that the client will check to decide if it needs to renew the token to get updated permission claims or impersonated claims data. Can you check my case?

Chad Boettcher
Chad Boettcher
2 years ago

Hello. Great article! I was wondering if you have provided, or plan to provide, the views or razor pages to manage the new features? Particularly, the roles to permissions, permissions, and users to roles maintenance.

Thanks!

Espen Gätzschmann
Espen Gätzschmann
2 years ago

This article series has been a great help to me when setting up a permissions-based system, though I’ve only used a small part of it. Thank you for writing it!

I have two questions:

1) If I want a method to require more than one permission, would I simply add the [HasPermission] attribute multiple times to that method?

2) I would like to enrich the 403 errors so that people using my API can get a better understanding of why they were denied access to a method. Is there a simple way to override the default 403 errors produced by this attribute so that I can replace them with my own, enriched versions? I have no trouble enriching errors in regular code, but it’s not quite as straightforward when using attributes.

Espen Gätzschmann
Espen Gätzschmann
2 years ago
Reply to  Jon P Smith

I think there are ways to override the behavior of HTTP codes from the basic authentication system, so that should be doable. I just hadn’t considered the fact that this uses the same system as the basic authentication layer.

The reason I asked about multiple attributes is because I’ve seen examples of multiple requirements by adding the attribute multiple times, but I have never tried this for myself. I wasn’t thinking about doing it like [HasPermission(Permissions.X, Permissions.Y)], but like [HasPermission(Permissions.X)] [HasPermission(Permissions.Y)]. But if you think this won’t work, that’s fine. Multiple permissions for a method would probably indicate a design flaw in my system anyway.

George Mukwewa
2 years ago

Awesome mate. Thank you!!!

Mark Chipman
Mark Chipman
2 years ago

… or perhaps integrating Identity Server 4 with your solution 🙂

Silvia
Silvia
2 years ago

Hi Jon, thank you a lot for this detailed tutorial. After struggling quite a little I was able to understand it and adopt it to my current project. I have one question concerning the superadmin: running the project it seems that the superadmin doesn’t have rows in the DataAccess table and, because of this, when I log in as the superadmin I am not able to display, for example, Shop Stocks. I think that you did this on purpose because the user impersonation feature comes to help and because each user can have only one Company at a time.

What if, in my project, I need the possibility to allow users (admin users) to be associated to many Tenants at a time? I think I could implement a List of DataKeys associated to the user in order to evaluate what the user can access. In addition it would be nice to have a superadmin user which can access all tenants. I’m not sure if it is more convenient to add DataAccess rows to the superadmin user for every new tenant that is added as well or to create a new Permission or something similar to check in HasQueryFilter method (similar to Permissions.AccessAll). It would be nice to hear your opinion/suggestion about this.

Jon P Smith
2 years ago
Reply to  Silvia

Hi Silvia,

I had to do the same thing for my client, i.e. an admin person could pick a company to access.My solution was crazy difficult, mainly because we were using Auth0 and they wanted all to happen on login.

For you I suggest you use the user impersonation code (see https://www.thereformedprogrammer.net/adding-user-impersonation-to-an-asp-net-core-web-application/ ), but maybe change it a bit. For instance if you created a company admin user that is locked to each company, i.e they have the DataKey for that company, whenever you created a company. Then you could put a front-end on impersonation code to show all the companies, and when they pick one you set them up as impersonating the company admin user for the company they picked.

If you are happy to dig into impersonation code then you can make that simpler – the key point is you need a way to change the DataKey in the current admin user’s claims and persist it to the authorization cookie.

Silvia
Silvia
2 years ago
Reply to  Jon P Smith

Hi Jon, thank your for the suggestion. I’m going to try with the impersonation code and see what happens.

Brendan Allen
Brendan Allen
2 years ago

I appreciate the vast amount of time and effort you have put into this and I’m using part of it in my application to save myself the time of figuring out how to properly store/retrieve permissions, etc. The only “critique” I could offer is, if you’re using .netcore 3.0 and you wanted to refresh claims, you could just call await userManager.UpdateSecurityStampAsync(user) after changing a user’s role and then ensure you have the following within within the startup.cs?
services.Configure(options =>
{
options.ValidationInterval = TimeSpan.Zero;
});

This would make it so that upon updating a user’s roles, you’d update their security stamp which will log them out instantly and you can just update the claims on login again. This avoids needing a whole system to track when a claim was last refreshed so that it’s only refreshed when it changes.

Jon P Smith
2 years ago
Reply to  Brendan Allen

Hi Brendan,

Thank you for your input. I’m always up for learning better ways to do things. What you describe sounds good, but aren’t there performance issues to setting the ValidationInterval to zero?? See this article https://security.stackexchange.com/questions/167832/asp-net-why-default-securitystamp-validation-interval-is-set-to-30-minutes

Brendan Allen
Brendan Allen
3 years ago
Reply to  Jon P Smith

That is definitely a fair point! Thank you for linking that post to me. I guess it depends on scale of the application and how many users you have concurrently. I’m currently managing an application that has about ~100 users and isn’t expected to get much higher so I can lower it to a zero or a minute and it’ll be “good enough” for my purposes. Again thank you for your time and effort in making these articles and a “framework” of sorts for easily implementing this into an ASP.Net Core MVC App.

kerpekri
kerpekri
2 years ago

Hey, thanks for the tutorial, but why do you have two different contexts Application & MyDBContext, two different databases for this? Whats the benefit?

Jon P Smith
2 years ago
Reply to  kerpekri

Hi kerpeki,

I did it so that it was easier for people to see what classes go with the “better authorization” code – just look at the ExtraAuthorizeDbContext and its associated classes.

In big projects I might use multiple DbContexts to help with building DDD “bounded Contexts”, but it has a number of down sides, like EF Core migrations are very complex and sometimes impossible (I don’t use EF Core migrations in my own projects).

I wouldn’t suggest using multiple DbContexts unless you have good reason to.

kerpekri
kerpekri
3 years ago
Reply to  Jon P Smith

Thanks for the super nice explanation.

Have a nice day && Loving your blog posts!

Rami M. Nassar
Rami M. Nassar
2 years ago

What is the max number of permission I can store per user in consideration of the cookie size? if for example I have a big application and average of thousand or two of permissions is it fine to store them in the cookie and keep it below 4kb of size?

Jon P Smith
2 years ago
Reply to  Rami M. Nassar

With the coding I used each permission takes 2 bytes. You also have some other claims which are stored in the cookie too, like the user Id. You can work out how much they take by adding the size of the key and value. Don’t forget: you might have thousands of permissions but how many does a single user have – most likely an admin user is the worst case.

For my client I was worried about this and used a more complex permissions setup with a type and then a series of increasing levels of allowed actions, e.g. OrderRead = xx0, OrderCreate = xx1 (which includes Read), OrderUpdate = xx2 (which includes Read and Create), etc. That reduces the permissions a LOT. But in the end we didn’t need it and it made adding permissions complex (I built a tool to build the permission from a json file).

Rami M. Nassar
Rami M. Nassar
3 years ago
Reply to  Jon P Smith

do you mean a permission code “1057” for example consume 2 bytes from the cookie size?
As you mentioned the biggest concern for users like admin and super users and while the application I’m referring to is growing this should be considered from the begging.

Jo Neve
Jo Neve
3 years ago
Reply to  Jon P Smith

Is it possible to share this code with the complex permissions setup?

Franjo Misetic
Franjo Misetic
2 years ago

Hello.
Is it a viable solution to implement IClaimsTransformation and not store claims in cookie/token, instead when a request comes in a IClaimsTransformation service does the permission claims setup and everything else works just the same? Only possible problem is that IClaimsTransformation gets called several times during request processing pipeline, so you would have to take that into account and take necessary precautions.

petar pan
petar pan
2 years ago

Hi Jon,
Thanks for the great article, really learned a lot!

You mentioned in the first part and in some comments that you implemented Auth0 and maybe different Front-End and that it was quite complex and challenging.
I am trying to do that now and struggling (React front and WebAPI back).

Can you share any hints of the key difficulties you encountered (article like this would be amazing:))?

Jon P Smith
2 years ago
Reply to  petar pan

Hi Petar,

The project was ASP.NET Core with Angular. The Angular person wanted to handle Auth0, but that wouldn’t work as we needed claims added to the ASP.NET Core’s user. Auth0 had example code that used 0Auth flow (this was about 2 years ago) but a quick look says they are now using OpenId. I’m sure there are other examples too.

The main problem was just understanding Auth0 and and setting up the machine-to-machine part. Like most complex things like this you have to chip away at a simple example until that works and then expand it.

All the best with your project!

petar pan
petar pan
2 years ago
Reply to  Jon P Smith

Thank you. Your project did help me a lot in the part of understanding of how things work.

Auth0 has a concept called Rules and they are simple JS functions that you can use to add claims to the token just like you showed.
I guess other providers have similar concepts.

That way I get a ready token and don’t have to do anything in the application itself.

Looks very promising!

Thank you!!!

bdcp
2 years ago

Hi Jon,

So if i understood correctly, if i’m using .NET Core Identity is:

UserToRoles equivalent to AspNetUserRoles
RolesToPermissions equivalent to AspNetUserRoles?

Jon P Smith
2 years ago
Reply to  bdcp

In ASP.NET Core identity you only have the Roles of a user – the AspNetUserRoles.But you don’t have a RolesToPermissions equivalent because the Roles are defined in the [Authorize] attribute, e.g. [Authorize( Roles = “Manager,Admin”)].

I suggest you read the very first article at https://www.thereformedprogrammer.net/a-better-way-to-handle-authorization-in-asp-net-core/ which explains what the normal use of Roles in ASP.NET Core does, and how this approach gives you the ability to change what a user can access dynamically, rather than having to edit your code and re-publish it.

bdcp
2 years ago
Reply to  Jon P Smith

Yea i understand that part, it’s just difficult to go from PermissionsAccessControl2 to a simple expansion of .NET Core identity within the same database, i was wondering how the tables would look like. But i don’t want to criticize, i guess you can’t please everyone.

Jon P Smith
2 years ago
Reply to  bdcp

Hi bdcp,

I’m not sure I understand what you are trying to do – are you trying to combine the class/tables in my extra database into the ASP.NET Core individual users account database? You could do that, but I kept them apart in the example application so that people could see what the different parts do.