What is a policy-based authorization?
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.
Why do we need policy-based authorization?
With policy-based authorization, we have custom logic to provide access to a resource by configuring the policy-based authorization.
Composing a policy for policy-based authorization
To create a policy-based authorization we need 2 things:
- A Policy Requirement: It is a collection of parameters that a policy can use when evaluating access to the user.
- A Policy Handler: This is responsible for evaluating against the given requirements and determining if access is allowed.
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.
Why do we care when the user object is null?
Well, authorization and authentication are two different things. The authorization handlers are still invoked even if the user is not authenticated.
We don’t have to explicitly specify
context.Fail()
instead we could just return a completed task (Task.CompletedTask
), which is treated as a failure if there are nocontext.Succeed
calls after executing the handler.
When should we call 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.
Adding authorization in Program.cs
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.
- PremiumOnly: We added the PremiumUser product code as a requirement to the policy. This allows only users who have
PremiumUser
product ID. - StandardOnly: We added the standard user product code as a requirement to the policy. This allows only users who have
StandardUser
product ID. - PremiumAndStandard: This access policy allows users to have premium user AND standard user access. The AND condition is because the
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
- Name of the policy
- An action delegate of type
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")]
Defining the policies on the controllers or razor pages
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
policy-based authorization demo
References
The post Policy-Based Authorization in ASP.NET Core appeared first on Code Rethinked.
Top comments (0)