Unlike role-based authorization, which solely depends on the roles assigned to the users. A policy-based authorization uses requirements that will provide access to a resource when succeeded.
A requirement is a collection of data used to evaluate the current user.
With policy-based authorization, we have custom logic to provide access to a resource by configuring the policy-based authorization.
To create a policy-based authorization we need 2 things:
Let’s say we have two pages on our website which represent two products. So, we will create two policies and the users who have access to the products will gain access to the resource.
Let’s create ProductAccessRequirement
and ProductAccessHandler
classes.
public class ProductAccessRequirement : IAuthorizationRequirement { public string[] ProductIds { get; set; } public ProductAccessRequirement(string[] productIds) { ProductIds = productIds; } }
Our ProductAccessRequirement
will accept a list of product ids as the requirements.
Notice we have implemented the IAuthorizationRequirement
interface for our requirement class. You will understand why we did it later in the article.
These will be configured in the Startup.cs
or Program.cs
(for .net 6+ programs) and will be utilized in the handler.
Here is our handler.
public class ProductAccessHandler : AuthorizationHandler<ProductAccessRequirement> { private readonly IUserAccessRepository _userAccessRepository; public ProductAccessHandler(IUserAccessRepository userAccessRepository) { _userAccessRepository = userAccessRepository; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ProductAccessRequirement requirement) { var user = ClaimExtensions.GetClaim(context.User, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "nameidentifier"); if (user == null) { context.Fail(); return Task.CompletedTask; } var hasAccess = _userAccessRepository.HasAccess(user.Value, requirement.ProductIds, CancellationToken.None); if (hasAccess) context.Succeed(requirement); return Task.CompletedTask; } }
Notice how we inherit from AuthorizationHanlder
abstract generic class and pass in our ProductAccessRequirement
as a type parameter.
We have to override the HandleRequirementAsync
method, which is executed when we declare our endpoints to have policy-based authorizations.
Look at how we are setting the context.Fail()
call when the user is null and the success call by invoking context.Succeed()
when the user has access to the product ids.
Well, authorization and authentication are two different things. The authorization handlers are still invoked even if the user is not authenticated.
context.Fail()
in authorization handlers?First things first, a requirement can have multiple handlers defined and registered.
When all the handlers for a requirement are executed and if we want to fail the authorization when the requirements are not satisfied in any of the handlers then invoking context.Fail()
this will force the failure even though the other handlers succeeded.
And we have a user repository, which will check whether the user has access to the product ids or not. For the user-to-product access map, I’ve created a dictionary with user ids and their respective product ids.
Here is our user access repository class.
public class UserAccessRepository : IUserAccessRepository { private readonly Dictionary<string, string[]> userIdToProductAccessMap = new Dictionary<string, string[]>() { { "2a3aa5e6-071d-4d40-8cf9-5f986c8e5ded", new string[] { Constants.ProductCodes.StandardUser, Constants.ProductCodes.PremiumUser } }, { "509bac4d-0648-4131-b0f5-52ce03a4069d", new string[] { Constants.ProductCodes.LimitedUser } }, { "6bdeb8ac-2eb4-4cc1-a3ab-ed848a6967a9", new string[] { Constants.ProductCodes.StandardUser } }, { "fc764407-c2d7-46b2-8652-9f7eb71dd5af", new string[] { Constants.ProductCodes.StandardUser, Constants.ProductCodes.PremiumUser } } }; public bool HasAccess(string userId, string[] products, CancellationToken cancellationToken) { var userExistsInList = userIdToProductAccessMap.TryGetValue(userId, out var userProductIds); if (!userExistsInList || userProductIds?.Any() == false) return false; return products.All(p => userProductIds.Contains(p)); } }
If we used a real database connection to fetch the user-related product ids, then the authorization will become slow as we’d have to fetch from the database for every user. I’d recommend using some caching system such as redis or Memcached to have those values without having much latency.
Now, let’s wire things up in the Program.cs
file (.NET 6 and later versions) or in Startup.cs
file (< .NET 6).
builder.Services.AddAuthorization(options => { options.AddPolicy("PremiumOnly", policy => policy.AddRequirements(new ProductAccessRequirement(new string[] { Constants.ProductCodes.PremiumUser }))); options.AddPolicy("StandardOnly", policy => policy.AddRequirements(new ProductAccessRequirement(new string[] { Constants.ProductCodes.StandardUser }))); options.AddPolicy("PremiumAndStandard", policy => policy.AddRequirements(new ProductAccessRequirement(new string[] { Constants.ProductCodes.StandardUser, Constants.ProductCodes.PremiumUser }))); });
We have defined 3 different policies and here is what they all mean.
PremiumUser
product ID.StandardUser
product ID.HasAccess
method defined in the user access repository will return true only if all the product IDs match in the requirements match with the user product IDs.Here is how the policies are created in the authorization options.
options.AddPolicy("PremiumOnly", policy => policy.AddRequirements(new ProductAccessRequirement(new string[] { Constants.ProductCodes.PremiumUser })));
The AddPolicy
method has two overloads. We used the one that takes the following 2 parameters
Action<AuthorizationPolicyBuilder>
We add requirements by calling the .AddRequirements
on the policy and passing our ProductAccessRequirement
as an argument.
The AddRequirements method accepts the instances of type IAuthorizationRequirement. This is why we have implemented our ProductAccessRequirement from IAuthorizationRequirement
interface though the interface has nothing to be implemented.
Now, all we have to do is add these policies to the authorize attributes. Ex: [Authorize(Policy = "PremiumOnly")]
Applying policies is the same for controllers and razor pages except that you cannot apply the authorize attribute to the razor page handlers.
For the purpose of the article, I’ve created two razor pages. One for the premium user and the other for a standard user.
[Authorize(Policy = "PremiumOnly")] public class PremiumLoungeModel : PageModel { public void OnGet() { } } [Authorize(Policy = "StandardOnly")] public class StandardLoungeModel : PageModel { public void OnGet() { } }
Here is what the lounge pages look when the user has access to them.
Within the _Layout.cshtml
file, I’ve included the links to these lounges like this to show these only if the user is signed in.
@if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark special-link" asp-area="" asp-page="/PremiumLounge" style="background: yellow;">Premium Lounge</a> </li> <li class="nav-item"> <a class="nav-link text-dark special-link-2" asp-area="" asp-page="/StandardLounge" >Standard Lounge</a> </li> }
For the purpose of this article, I’ve included the premium lounge link to the signed-in user as well so that everyone can see it but if they try to access the lounge they should get an access denied message.
Here is how the access denied error looks for both standard and premium lounge pages.
Let’s see how these policies work with a quick demo
Karthik is a passionate Full Stack developer working primarily on .NET Core, microservices, distributed systems, VUE and JavaScript. He also loves NBA basketball so you might find some NBA examples in his posts and he owns this blog.
In this post, we’ll see how to test gRPC Server applications using different clients. And… Read More
In this post, we'll create a new gRPC project in ASP.NET Core and see what's… Read More
In this blog post, we’ll see how to run dotnet core projects without opening visual… Read More
Programmatically evaluating policies is useful when we want to provide access or hide some data… Read More
We saw how we could set up policy-based authorization in our previous article. In this… Read More
What is role-based authorization? As the name says, role-based authorization authorizes a user based on… Read More
This website uses cookies.