ASP.Net Core Authorization (Role-based and Policy-based Authorization)

In this blog, I am going to take a deep-dive into ASP.Net Core Authorization. Authorization is the process to find out what action a user can perform. In the case of a REST API, it can be the resources a user can access. Or a particular HTTP verb associated with a resource.

For example, let us say we have an e-commerce inventory management application. In the application, the warehouse manager manages the record of the entire inventory of the warehouse. And let us say that there is a REST API for Inventory. Also, the API exposes GET to return all items in the inventory. Plus a POST to add items to the inventory. In this case, the warehouse manager with administrative privilege will be able to access both the GET and POST methods of the Inventory resource. Whereas a warehouse employee will be able to access only the GET method of the Inventory resource.

For authorization to work, the user has to be authenticated first. This is something I discussed in my previous blog on authentication. To find out what a user can access we need the user’s identity. And the identity of an user is set only after the user is authenticated.

To understand the code examples here and how it ties with authentication I strongly suggest to visit my previous blog on authentication.

Types of ASP.Net Core Authorization

ASP.Net core authorization mechanism provides two types of implementation:

  • Role-based authorization
  • Policy-based authorization

Role-based ASP.Net Core Authorization

In role-based authorization, we perform authorization checks with an attribute-based declaration. We will use AuthorizeAttribute attribute in the method which we want to allow access to a specific role. And the role is part of the Identity of a user.

An important point to note, based on application rules, a single user can have multiple roles.

From the above example of the inventory management system’s Inventory REST API; if we have to configure the GET and POST methods through AuthorizeAttribute attribute, the code will look like below.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Auth.Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class InventoryController : ControllerBase
    {
        // GET: api/Inventory
        [Authorize(Roles = "Administrator, User")]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // POST: api/Inventory
        [Authorize(Roles = "Administrator")]
        [HttpPost]
        public void Post([FromBody] Inventory value)
        {
        }
    }
}

For a method, if we want to provide multiple roles access, we can either add the AuthorizeAttribute attribute multiple times or provide a comma-separated list of roles for the AuthorizeAttribute attribute.

For our application, the administrator (warehouse manager) role, as well as the user role, both can access the GET method of the InventoryController class. Below example illustrates this using two attributes. One for the Administrator role and another one for the User role.

// GET: api/Inventory
[Authorize(Roles = "Administrator")]
[Authorize(Roles = "User")]
[HttpGet]
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

Or we can also declare it as shown below.

// GET: api/Inventory
[Authorize(Roles = "Administrator, User")]
[HttpGet]
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

I personally prefer the comma-separated list of roles in the AuthorizeAttribute attribute. In my opinion, it makes code more concise, but it is just my personal preference.

NOTE: The Inventory class which is in the input parameter of the POST method will be an empty class for now.

Add roles to Identity

For the role-based authorization to work, we will first need to add roles to identity. Hence, I will update the authentication handler to add roles to identity. In my previous blog, I created a custom authentication handler class named CustomAuthenticationHandler. I will now modify the class to add roles to identity.

Inside the CustomAuthenticationHandler when I create an instance of GenericPrincipal class, I need to pass the role that principal belongs to. But to create a mapping of the user to the roles, I will first update CustomAuthenticationManager to provide the mapping.

New User class

To achieve this, I will first create a new type User, which will have the Username, Password, and Role property.

namespace Auth.Demo
{
    public class User
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public string Role { get; set; }
    }
}

Secondly, I will update the class variable users inside CustomAuthenticationManager to be of the type IList of type User.

private readonly IList<User> users = new List<User>
        { new User { Username= "test1", Password= "password1", Role= "User"  },
          new User { Username= "test2", Password= "password2", Role="Administrator" }
        };

Thirdly, since I will have to have both authentication token as well as the role of a user, I will update the ICustomAuthenticationManager interface. And change the return type for the property Tokens. In the return IDictionary with user name as a key, the value will be a Tuple of token and role.

using System;
using System.Collections.Generic;

namespace Auth.Demo
{
    public interface ICustomAuthenticationManager
    {
        string Authenticate(string username, string password);

        IDictionary<string, Tuple<string, string>> Tokens { get; }
    }
}

Fourthly, I will change the implementation of the method Authenticate of the class CustomAuthenticationManager, to add the role of the user along with the authentication token for Identity creation.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Auth.Demo
{
    public class CustomAuthenticationManager : ICustomAuthenticationManager
    {
        private readonly IList<User> users = new List<User>
        { new User { Username= "test1", Password= "password1", Role= "User"  },
          new User { Username= "test2", Password= "password2", Role="Administrator" }
        };

        private readonly IDictionary<string, Tuple<string,string>> tokens = 
            new Dictionary<string, Tuple<string, string>>();

        public IDictionary<string, Tuple<string, string>> Tokens => tokens;

        public string Authenticate(string username, string password)
        {
            if (!users.Any(u => u.Username == username && u.Password == password))
            {
                return null;
            }

            var token = Guid.NewGuid().ToString();

            tokens.Add(token, new Tuple<string, string>(username, 
                users.First(u => u.Username == username && u.Password == password).Role));

            return token;
        }
    }
}
Changing CustomAuthenticationHandler

Finally, I will update CustomAuthenticationHandler class to add a role to the user’s identity. I will change the ValidateToken private method. And use the role returned from the Tokens property of the CustomAuthenticationManager class. And pass it to the constructor of the GenericPrincipal class during instantiation. I will also update the list of Claim to add a Claim for the ClaimTypes.Role along with the role of the user.

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace Auth.Demo
{
    public class BasicAuthenticationOptions : AuthenticationSchemeOptions
    { }

    public class CustomAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
    {
        private readonly ICustomAuthenticationManager customAuthenticationManager;

        public CustomAuthenticationHandler(
            IOptionsMonitor<BasicAuthenticationOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            ICustomAuthenticationManager customAuthenticationManager) :
            base(options, logger, encoder, clock)
        {
            this.customAuthenticationManager = customAuthenticationManager;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Unauthorized");

            string authorizationHeader = Request.Headers["Authorization"];
            if (string.IsNullOrEmpty(authorizationHeader))
                return AuthenticateResult.Fail("Unauthorize");

            if (!authorizationHeader.StartsWith("bearer", StringComparison.OrdinalIgnoreCase))
                return AuthenticateResult.Fail("Unauthorize");

            string token = authorizationHeader.Substring("bearer".Length).Trim();

            if (string.IsNullOrEmpty(token))
                return AuthenticateResult.Fail("Unauthorize");

            try
            {
                return ValidateToken(token);
            }
            catch (Exception ex)
            {
                // Log 
                return AuthenticateResult.Fail("Unauthorize");
            }
        }

        private AuthenticateResult ValidateToken(string token)
        {
            var validatedToken = customAuthenticationManager.Tokens.FirstOrDefault(t => t.Key == token);
            if (validatedToken.Key == null)
            {
                return AuthenticateResult.Fail("Unauthorize");
            }

            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, validatedToken.Value.Item1),
                new Claim(ClaimTypes.Role, validatedToken.Value.Item2)
            };

            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new GenericPrincipal(identity, new[] { validatedToken.Value.Item2 });
            var ticket = new AuthenticationTicket(principal, Scheme.Name);
            return AuthenticateResult.Success(ticket);
        }
    }
}

Now that the authentication pipeline work is complete to add the user’s role to the Identity, it is time to test the InventoryController.

Test Role-based authorization

Firstly, I will create an auth token for a user with the Administrator role and check to see the user should have access to both the GET and POST method. I will use Postman for the REST calls.

administrator authentication token

Secondly, I will call the GET method of the InventoryController with the auth token.

administrator role get authorization

Thirdly, I will test the POST method with the same administrator token. And the response returned will be 200 success.

administrator role post

Fourthly, I will create an auth token for the user role.

user authentication token

Next, I will call GET with the user’s auth token.

user role get asp.net core authorization

Finally, I will call POST with the user’s auth token. And in this case I will get an HTTP status code 403 Forbidden. This is the expected result since the User role does not have access to POST.

user role post ASP.Net Core Authorization

Policy-based ASP.Net Core Authorization

With role-based authorization, flexibility is very limited. It is just a role can either access a resource or it cannot. If we want to do some custom logic irrespective of the role and based on that authorize an identity, we need policy-based authorization.

In policy-based authorization, a policy consists of three main parts:

  • One or more requirements. The requirement of a policy is a data collection the policy handler uses to implement the logic of the policy.
  • And each requirement contains a handler. Most importantly, the handler is responsible for doing the logic for the authorization checks.

Another place where policy-based authorization comes really handy is when you have multiple roles in an application. Passing different permutation and a combination of roles in the AuthorizeAttribute attribute is cumbersome. And we can easily create policy based on multiple role combinations.

Policy-based on multiple roles

In the previous example of InventoryController, let us consider we also have a Poweruser role. And the POST method is now accessible by Poeweruser as well as Administrator. Though this is a trivial example, I will demonstrate how we can use policy-based authorization here.

Firstly, I will update the Startup class and change the ConfigureServices method. In the ConfigureServices method I will call AddAuthorization method on the IServiceCollection instance to add a policy. I will add a simple policy to combine the Administrator role and Poweruser role.

services.AddAuthorization(options => 
            {
                options.AddPolicy("AdminAndPoweruser",
                    policy => policy.RequireRole("Administrator", "Poweruser"));
            });

The complete Startup code below:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace Auth.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            var tokenKey = Configuration.GetValue<string>("TokenKey");
            var key = Encoding.ASCII.GetBytes(tokenKey);

            services.AddAuthentication("Basic")
                .AddScheme<BasicAuthenticationOptions, CustomAuthenticationHandler>("Basic", null);

            services.AddAuthorization(options => 
            {
                options.AddPolicy("AdminAndPoweruser",
                    policy => policy.RequireRole("Administrator", "Poweruser"));
            });

            services.AddSingleton<ICustomAuthenticationManager, CustomAuthenticationManager>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Secondly, I will update the POST method of the InventoryController. Instead of using AuthorizeAttribute with a Role, I will use AuthorizeAttribute with Policy. And I will pass the newly created policy here.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Auth.Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class InventoryController : ControllerBase
    {
        // GET: api/Inventory
        [Authorize(Roles = "Administrator, User")]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // POST: api/Inventory
        [Authorize(Policy = "AdminAndPoweruser")]
        [HttpPost]
        public void Post([FromBody] Inventory value)
        {
        }
    }
}

Finally, I will run the same Postman tests and before. And the results will be the same as expected.

Using custom authorization handler

In our example of the Inventory management system, let us change the requirement for the POST. Let us say that even employee’s who are with the organization for more than 20 years can also create new inventory. Now, this requirement complements the authorization a little.

Using a plain policy will not work anymore. So in this, our implementation for a policy will be different. Instead of using the available RequireRole of the AuthorizationPolicyBuilder instance, we will add a new requirement. And for that, we will use the Requirements property of the AuthorizationPolicyBuilder instance.

A new requirement

Firstly, to add a new requirement to the Requirements collection, we will create a new class implementing the IAuthorizationRequirement interface. I will name the class EmployeeWithMoreYearsRequirement. And this new class with accepting the number of years required in the constructor.

using Microsoft.AspNetCore.Authorization;

namespace Auth.Demo
{
    public class EmployeeWithMoreYearsRequirement : IAuthorizationRequirement
    {
        public EmployeeWithMoreYearsRequirement(int years)
        {
            Years = years;
        }

        public int Years { get; set; }
    }
}

Secondly, I will update the Startup class ConfigureServices method to add a new policy. This time the new policy will just add the new requirement class we added to the existing requirements.

options.AddPolicy("EmployeeMoreThan20Years", policy => policy.Requirements.Add(new EmployeeWithMoreYearsRequirement(20)));

The complete Startup class code:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace Auth.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            var tokenKey = Configuration.GetValue<string>("TokenKey");
            var key = Encoding.ASCII.GetBytes(tokenKey);

            services.AddAuthentication("Basic")
                .AddScheme<BasicAuthenticationOptions, CustomAuthenticationHandler>("Basic", null);

            services.AddAuthorization(options => 
            {
                options.AddPolicy("AdminAndPoweruser",
                    policy => policy.RequireRole("Administrator", "Poweruser"));
                options.AddPolicy("EmployeeMoreThan20Years",
                    policy => policy.Requirements.Add(new EmployeeWithMoreYearsRequirement(20)));
            });

            services.AddSingleton<ICustomAuthenticationManager, CustomAuthenticationManager>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Now that the wiring up of the policy is done, it is time to create the policy handler. I will also create a new provider class, which will return an employee’s number of years in the service.

The Policy handler

Firstly, I will create a new class that is responsible for returning the number of years of service of an employee. For simplicity, I will create a hardcoded implementation. The implementation will always return 21 for user "test1", otherwise it will return 10.

In a real-life scenario, this value will come from some sort of data store.

namespace Auth.Demo
{
    public interface IEmployeeNumberOfYearsProvider
    {
        int Get(string value);
    }
}

namespace Auth.Demo
{
    public class EmployeeNumberOfYearsProvider : IEmployeeNumberOfYearsProvider
    {
        public int Get(string value)
        {
            if(value == "test1")
            {
                return 21;
            }
            return 10;
        }
    }
}

Secondly, I will create a new handler for the policy. Instead of implementing the IAuthorizationHandler, I will derive from the AuthorizationHandler abstract class. I will name the new class EmployeeWithMoreYearsHandler.

Inside of the new class, I will first check if the ClaimTypes.Name is available, since I will need the username to find the years in service. After that, I will get the number of years in service from the EmployeeNumberOfYearsProvider instance. And finally, I will compare it against the requirement, and if it matches the criteria I will send a success.

using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Auth.Demo
{
    public class EmployeeWithMoreYearsHandler : AuthorizationHandler<EmployeeWithMoreYearsRequirement>
    {
        private readonly IEmployeeNumberOfYearsProvider employeeNumberOfYearsProvider;

        public EmployeeWithMoreYearsHandler(IEmployeeNumberOfYearsProvider employeeNumberOfYearsProvider)
        {
            this.employeeNumberOfYearsProvider = employeeNumberOfYearsProvider;
        }

        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context, 
            EmployeeWithMoreYearsRequirement requirement)
        {
            if (!context.User.HasClaim(c => c.Type == ClaimTypes.Name))
            {
                return Task.CompletedTask;
            }

            var name = context.User.FindFirst(c => c.Type == ClaimTypes.Name);

            int numberofyears = employeeNumberOfYearsProvider.Get(name.Value);

            if(numberofyears >= requirement.Years)
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Finally, I will update the Startup class to register both EmployeeWithMoreYearsHandler and EmployeeNumberOfYearsProvider to the dependency injection container.

using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Auth.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            var tokenKey = Configuration.GetValue<string>("TokenKey");
            var key = Encoding.ASCII.GetBytes(tokenKey);

            services.AddAuthentication("Basic")
                .AddScheme<BasicAuthenticationOptions, CustomAuthenticationHandler>("Basic", null);

            services.AddAuthorization(options => 
            {
                options.AddPolicy("AdminAndPoweruser",
                    policy => policy.RequireRole("Administrator", "Poweruser"));
                options.AddPolicy("EmployeeMoreThan20Years",
                    policy => policy.Requirements.Add(new EmployeeWithMoreYearsRequirement(20)));
            });

            services.AddSingleton<IAuthorizationHandler, EmployeeWithMoreYearsHandler>();
            services.AddSingleton<IEmployeeNumberOfYearsProvider, EmployeeNumberOfYearsProvider>();
            services.AddSingleton<ICustomAuthenticationManager, CustomAuthenticationManager>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}
Updating the Inventory controller

Now, I will update the InventoryController‘s POST method. I will change the AuthorizeAttribute to use the newly created policy.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Auth.Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class InventoryController : ControllerBase
    {
        // GET: api/Inventory
        [Authorize(Roles = "Administrator, User")]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // POST: api/Inventory
        [Authorize(Policy = "EmployeeMoreThan20Years")]
        [HttpPost]
        public void Post([FromBody] Inventory value)
        {
        }
    }
}

Once I finish the controller code, I will test the code using Postman. And the test results will be exactly same as before and as expected. For the "user1" who has more than 20 years of experience in the organization can access the POST method. Whereas the "user2" will get 403 Forbidden error.

Conclusion

The policy-based authorization makes the ASP.Net Core Authorization system extremely powerful. For basic requirements, role-based authorization does the job. But when it comes to real complex requirements, like the one I have demonstrated above, policy-based authorization makes it real easy.

And with policy-based authorization, the code becomes really clean. As the business logic is completely isolated from the authorization code.

Microsoft documentation in authorization is available here: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1

Source code location: https://github.com/choudhurynirjhar/auth-demo

YouTube video on the role-based authorization is: https://youtu.be/-N6O2rtCdI8