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.
Secondly, I will call the GET method of the InventoryController
with the auth token.
Thirdly, I will test the POST method with the same administrator token. And the response returned will be 200 success.
Fourthly, I will create an auth token for the user role.
Next, I will call GET with the user’s auth token.
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.
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