In this article, we will show you how to implement Resource-based authorization in ASP.NET Core. In Resource-based authorization, we inject the Authorization Service in the controller or Razor Page. We then call the AuthorizeAsync
method to run an authorization check.
Table of Contents
The authorization policies that we built in the previous tutorial depend on Authorize
attribute. The Authorization middleware reads the Authorize
attribute and runs the checks to see if it can authorize the user.
The Authorization middleware runs before data binding and before execution of the action method (or page handler). Hence it does not have access to the data (resources) on which the action method operates.
But, there are many use cases that exist, where we need to load resources first, before determining if we can allow the user to access those resources. The Attribute
-based authorization mechanism does not handle such use cases.
For Example, we want to allow only the creator of the document to edit or delete it. The other users can only view the document. In this scenario, we need to load the document first. We will then check if the creator of the document is the same as the logged-in user. Only then we can allow users to access the document.
Resource-based on Authorization is an imperative authorization. Here we do not rely on the Authorize
attribute, But we choose when to call the Authorization Service.
We do that by injecting the Authorization Service into our controller class. In the action method, we load the resource first. After that, we call the AuthorizeAsync
method passing the resource to it along with the policy that we want to apply. Once the method returns the result, we check it and take appropriate action.
Writing a resource-based handler is very similar to creating custom policy-based authorization. We learned how to create a policy-based authorization in the previous tutorial.
To Create a Resource-based Authorization
- Create an Authorization Requirement. The Requirement class must inherit from the IAuthorizationRequirement
- Next, create Authorization Handlers. The class must
AuthorizationHandler<SameAuthorRequirement, Document>
- Register the Authorization Handlers for DI
- Create a Policy using the Authorization Requirement
- Inject the Authorization Service in the controller class
- Call the
AuthorizeAsync
method, wherever you want to check for Authorization.
We will continue from where we left from our last tutorial on Policy based Authorization
Create a class SameAuthorRequirement
in the Authorization
folder, which will act as a requirement for our policy.
A Requirement class must implement IAuthorizationRequirement
from the Microsoft.AspNetCore.Authorization
namespace. IAuthorizationRequirement
does not contain any methods or properties. Hence we do not need to implement anything.
1 2 3 4 5 6 7 8 9 10 | using Microsoft.AspNetCore.Authorization; namespace AuthzExample.Authorization { public class SameAuthorRequirement : IAuthorizationRequirement { } } |
Our Next task is to create an Authorization handler for the above requirement. The authorization handler contains the logic to determine if the requirement is Valid.
An Authorization Requirement can contain more than one Authorization Handlers
Create DocumentAuthorizationHandler
in the Authorization
folder.
The handlers must inherit from the AuthorizationHandler<TRequirement, TResource>
. It has one method HandleRequirementAsync
, which we must implement in our handler class.
Note that in policy-based Authorization, we inherited from AuthorizationHandler<TRequirement>
. That is the only difference between resource-based implementation Vs policy-based implementation
The HandleRequirementAsync
method gets the context
, requirement
& resource
as the parameter. The context
class has User
(ClaimPrincipal
) object.
In the code, we check to see if the CreatedUserID of the Product (resource
) is the same as that of the logged-in user. If true
return Succeed
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using AuthzExample.Data; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; using System.Threading.Tasks; namespace AuthzExample.Authorization { public class DocumentAuthorizationHandler : AuthorizationHandler<SameAuthorRequirement, Product> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SameAuthorRequirement requirement, Product resource) { if (context.User.HasClaim(ClaimTypes.NameIdentifier, resource.CreatedUserID)) { context.Succeed(requirement); } return Task.CompletedTask; } } } |
Registering the Handlers
Register the handler in the ConfigureServices
method of the startup class..
1 2 3 | services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>(); |
Create the Policy based on Requirement
1 2 3 4 5 6 7 | options.AddPolicy("sameAuthorPolicy", policy => policy.AddRequirements( new SameAuthorRequirement() )); |
Using the Authorization Service
We want to limit access to the Edit method to the user who created the product. Open the MVCProductsController
First, we inject the IAuthorizationService
in the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | namespace AuthzExample.Controllers { public class MVCProductsController : Controller { private readonly ApplicationDbContext _context; private readonly IAuthorizationService _authorizationService; public MVCProductsController(ApplicationDbContext context, IAuthorizationService authorizationService) { _context = context; _authorizationService = authorizationService; } |
In the Edit
method, first get the Product
from the back end.
Call the method AuthorizeAsync
with User
, Product
and policy
name. The AuthorizeAsync
invokes the DocumentAuthorizationHandler
with the given Product
.
DocumentAuthorizationHandler
matches the UserId
of the Product
with the UserID
of the logged-in user. If they match return Sucess.
We check if the authorization has passed from the variable result.Succeeded
. If the authorization fails, then check if the user is authenticated. If not then redirect the user to the login screen else redirect to the access denied page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // GET: MVCProducts/Edit/5 public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var product = await _context.Products.FindAsync(id); if (product == null) { return NotFound(); } var result = await _authorizationService.AuthorizeAsync(User, product, "sameAuthorPolicy"); if (!result.Succeeded) { if (User.Identity.IsAuthenticated) { return new ForbidResult(); } else { return new ChallengeResult(); } } return View(product); } |
Finally, last important point. Remember to update CreatedUserID
with the currently logged-in user in the Create
method. Also, remove CreatedUserID
from the forms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("ProductId,Name,CreatedUserID")] Product product) { if (ModelState.IsValid) { product.CreatedUserID = this.User.FindFirstValue(ClaimTypes.NameIdentifier); _context.Add(product); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(product); } |
Now, run the application.
Create two users. Log in using one user and create a Product. Now log out and try to edit the product using another user. You should see the access denied page.
Reference
Read More
- ASP.NET Core Tutorial
- Authentication in ASP.NET Core
- Cookie Authentication in ASP.NET Core
- Introduction to ASP.NET Core Identity
- ASP.NET Core Identity Tutorial From Scratch
- Sending Email Confirmation in ASP.NET Core
- Add Custom Fields to the user in ASP.NET Core Identity
- Change Primary key in ASP.NET Core Identity
- JWT Authentication in ASP.NET Core
- Introduction to Authorization
- Simple Authorization using Authorize attribute
- Adding & Managing Claims in ASP.NET Core Identity
- Claim Based Authorization in ASP.NET Core
- Policy-based Authorization
- Resource-Based Authorization