This article describes patterns and methods available in ASP.NET Core MVC.
I would like to emphasize that we will explore only authorization (a process of verifying user’s rights), rather than authentication. Thus, we will not use ASP.NET Identity, authentication protocols, etc. In addition, we will have a look at some examples of using server code and Core MVC source code. At the end of the article, you will find a link to a test project.
Logo designed by Pablo Iglesias.
Contents:
- Claims
- Preparatory work
- Authorize attribute and access policy
- Access policy settings
- Resource-based authorization
- Authorization in Razor markup
- Permission-based authorization. Custom authorization filter
Claims
Authorization and authentication principles in ASP.NET Core MVC have not been much changed compared to the previous framework version. They differ in details. One of the new notions is ‘claim-based authorization’.
What is a claim? It is a key-value pair where ‘FirstName’, ‘EmailAddress’ can be used as a key. Thus, we can say that a claim is a property of the user, a string with data, or a statement like ‘A user has something’. Familiar to many developers, the one-dimensional role-based model is inherently contained in the multidimensional claim-based model: the Role (the “User has an X role” claim) is one of the claims and is contained in the list of pre-defined System.Security.Claims.ClaimTypes. You can create custom claims.
The next important notion is ‘identity’. This is a single statement containing a claim set. Thus, identity can be interpreted as a whole document (passport, driving license, etc.). In this case, a claim is the line in the passport (date of birth, surname, etc). Core MVC uses the System.Security.Claims.ClaimsIdentity.
There is another notion — principal, which is at a higher level and denotes the user itself. As in real life, a person can have multiple documents at the same time, in Сore MVC, the principal can contain multiple user-associated identities.
The HttpContext.User property has the System.Security.Claims.ClaimsPrincipal type. It is obvious that you can get all claims of each identity using a principal. A set of more than one identities can be used to delimit access to different sections of a site/service.
This diagram shows only several System.Security.Claims class properties and methods.
Why do we need all this? While using the claim-based authorization, we implicitly specify that a user needs to have a necessary claim (a property of the user) to access the resource. In the simplest case, the actual existence of a certain claim is checked, although there may be more complex combinations (set through policy, requirements, permissions – we’ll look at these concepts in detail below). For example, to drive a car, you need to have a driving license (identity) with the В category (claim).
Preparatory work
Here and throughout the article, we will set up access for different pages in the Web site.
To run the code, you need to create a new ASP.NET Core Web application in Visual Studio 2015, set the Web Application template, and specify the authentication type as “no authentication”.
When using the “Individual User Accounts” authentication, the code to store and load users in a database would be generated with ASP.NET Identity, EF Core and localdb. This is completely redundant under this article, even though there is a lightweight testing solution like EntityFrameworkCore.InMemory.
Moreover, we do not need the ASP.NET Identity authentication library. The process of obtaining the principal for authorization can be emulated in-memory. The principal serialization in a cookie is possible by standard core MVC tools. That is all we need for our testing.
How to use the ASP.NET Identity with the in-memory repository of users.
To emulate the repository of users, you need to open Startup.cs and register stub services in the built-in DI-container:
public void ConfigureServices(IServiceCollection services) { //enable Identity services.AddIdentity<IdentityUser, IdentityRole>(); //register a storage services.AddTransient<IUserStore<IdentityUser>, FakeUserStore>(); services.AddTransient<IRoleStore<IdentityRole>, FakeRoleStore>(); }
In addition, we completed the same task, as AddEntityFrameworkStores<TContext> would do:
services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<IdentityDbContext>();
We will start with the user authorization on the site. Create a stub on GET /Home/Login, and add a button to send an empty form to the server. On POST /Home/Login, create principal, identity, and claim (in a real application we would retrieve this data from the database).
The HttpContext.Authentication.SignInAsync call serializes principal and inserts it into the encoded cookie, which will be attached to the webserver response and stored on the client’s side.
The code of creating principal stub when signing in the site:
[HttpGet] [AllowAnonymous] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel vm, string returnUrl = null) { //TODO: проверка пароля, загрузка пользователя из БД, и т.д. и т.п. var claims = new List<Claim> { new Claim(ClaimTypes.Name, "Fake User"), new Claim("age", "25", ClaimValueTypes.Integer) }; var identity = new ClaimsIdentity("MyCookieMiddlewareInstance"); identity.AddClaims(claims); var principal = new ClaimsPrincipal(identity); await HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal, new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(20) }); _logger.LogInformation(4, "User logged in."); return RedirectToLocal(returnUrl); }
Then, enable the cookie authentication in the Startup.Configure(app) method:
app.UseCookieAuthentication(new CookieAuthenticationOptions() { AuthenticationScheme = "MyCookieMiddlewareInstance", CookieName = "MyCookieMiddlewareInstance", LoginPath = new PathString("/Home/Login/"), AccessDeniedPath = new PathString("/Home/AccessDenied/"), AutomaticAuthenticate = true, AutomaticChallenge = true });
This code with few modifications will be the base for the further examples.
Authorize attribute and access policy
The [authorize] attribute has not gone anywhere from MVC.
Still, if you mark Controller/action with this attribute, only an authorized user will be able to access the inside. Things become more interesting if you specify the policy name, which is a requirement for a claim user:
[Authorize(Policy = "age-policy")] public IActionResult About() { return View(); }
Policies are created in the familiar Startup.ConfigureServices method.
services.AddAuthorization(options => { options.AddPolicy("age-policy", x => { x.RequireClaim("age"); }); });
This policy defines that only the authorized user with the ‘age’ claim can access the About page. The claim value is not taken into account.
Now, we are going to explore how it works inside.
[Authorize] is an attribute without logic. It only needs to specify the MVC to which controller/action it should connect Authorizefilter — one of the built-in core MVC filters.
The concept of filters is as the same as in the previous framework versions: filters are executed sequentially and allow executing the code before and after accessing the controller/action.
Its main difference from middleware is as follows: filters have access to the specific for MVC context and are executed after all middleware. However, this distinction is blurred, as the call of middleware may be inserted in the filter pipeline using the [MiddlewareFilter] attribute.
Now, let’s talk about the authorization and AuthorizeFilter. I would like to draw your attention to the OnAuthorizationAsync method:
- From the policy list, it selects the required policy based on the specified value of the [Authorize] attribute or selects AuthorizationPolicy – a default policy containing only one requirement with DenyAnonymousAuthorizationRequirement.
- It checks whether the set from identity and user’s claims matches the policy requirements.
Access policy settings
Creating access policies through the fluent-UI does not give you the required flexibility that you may need in real-world applications. Of course, you can explicitly specify valid claim values through a call to RequireClaim(“x”, params values).
Alternatively, you can combine them with logic conditions using the RequireClaim(“x”).RequireClaim(“y”) call. Finally, you can assign different policies to controller and action, which will lead to the same combination of conditions using the logic AND. Obviously, it is necessary to use a more flexible mechanism of creating policies – requirements and handlers, and we have one.
services.AddAuthorization(options => { options.AddPolicy("age-policy", policy => policy.Requirements.Add(new AgeRequirement(42), new FooRequirement())); });
Requirement is no more than DTO to pass parameters to the appropriate handler, which in turn has access to the HttpContext.User and can impose any checks on the principal and the identity/claim contained therein.
Moreover, a handler can receive external dependencies through the built-in DI-container.
public class MinAgeRequirement : IAuthorizationRequirement { public MinAgeRequirement(int age) { Age = age; } public int Age { get; private set; } } public class MinAgeHandler : AuthorizationHandler<MinAgeRequirement> { public MinAgeHandler(IFooService fooService) { // fooService will passed using DI } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinAgeRequirement requirement) { bool hasClaim = context.User.HasClaim(c => c.Type == "age"); bool hasIdentity = context.User.Identities.Any(i => i.AuthenticationType == "MultiPass"); string claimValue = context.User.FindFirst(c => c.Type == "age").Value; if (int.Parse(claimValue) >= requirement.Age) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
Now, register the handler in Startup.ConfigureServices():
services.AddSingleton<IAuthorizationHandler, MinAgeHandler>();
You can combine handlers both with AND and OR. In this case, when registering several inheritants of AuthorizationHandler<FooRequirement>, they all will be called. In this case, the context.Succeed( call is optional, while the context.Fail() call leads to the common cancellation in the authorization regardless the outcome of other handlers. Thus, we can combine the analyzed mechanisms in the following ways:
- Policy: AND
- Requirement: AND
- Handler: AND / OR.
Resource based authorization
As we have already discussed, a policy-based authorization is executed in the filter pipeline before the call of protected action. Whether authorization is successful depends on the user – either they have the required claim, or do not have. What if you want to consider the protected resource and its properties, and get data from external sources?
Here is a real example: we protect an action of the GET /Orders/{id} type, which reads a row with the order from the database by id. Assume we can determine whether the user has the rights to the particular order only after retrieving it from the database. This automatically makes the previously discussed aspect-based scenarios on MVC filters to be useless. In addition, they are executed before the user code gets control. Fortunately, in Core MVC, there are ways to perform authorization manually.
To do this, the controller should enable the IAuthorizationService implementation. We will get it by implementing the dependency into the kit.
public class ResourceController : Controller { IAuthorizationService _authorizationService; public ResourceController(IAuthorizationService authorizationService) { _authorizationService = authorizationService; } }
Then, we will create a new policy and handler:
options.AddPolicy("resource-allow-policy", x => { x.AddRequirements(new ResourceBasedRequirement()); }); public class ResourceHandler : AuthorizationHandler<ResourceBasedRequirement, Order> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, ResourceBasedRequirement requirement, Order order) { // TODO: to verify if a user has rights to work with the order if (true) context.Succeed(requirement); return Task.CompletedTask; } }
Finally, we will check if the user and resource match the required policy inside the action. In addition, it should be noted that the [Authorize] attribute is not used any more:
public async Task<IActionResult> Allow(int id) { Order order = new Order(); //get resource from the database if (await _authorizationService.AuthorizeAsync(User, order, "my-resource-policy")) { return View(); } else { //returns 401 or 403 depending on the user return new ChallengeResult(); } }
In the IAuthorizationService.AuthorizeAsync method, a starvation takes a list from the requirement instead of the policy name:
Task<bool> AuthorizeAsync( ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
This allows setting up access rights in a more flexible way. For a demo version, we will use OperationAuthorizationRequirement specified as before (I have taken this example from docs.microsoft.com):
public static class Operations { public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; public static OperationAuthorizationRequirement Read = new OperationAuthorizationRequirement { Name = "Read" }; public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = "Update" }; public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; }
We get the following result:
_authorizationService.AuthorizeAsync( User, resource, Operations.Create, Operations.Read, Operations.Update);
In the HandleRequirementAsync(context, requirement, resource) method of the corresponding handler, it is necessary to simply validate rights of the corresponding operation specified in requirement.Name and to call context.Fail() if the user failed to authorize:
protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, Order order) { string operationName = requirement.Name; // Verify if the user has rights to work with the order if(true) context.Succeed(requirement); return Task.CompletedTask; }
How many you call the handler will depend on the amount of passing requirements in AuthorizeAsync. In addition, it will check each requirement separately. To verify all rights to the operations for a handler call, it is necessary to pass a list of operations inside requirements:
new OperationListRequirement(new[] { Ops.Read, Ops.Update })
The review of resource-based possibilities is over. Thus, it is high time to cover the handlers with the tests:
[Test] public async Task MinAgeHandler_WhenCalledWithValidUser_Succeed() { var requirement = new MinAgeRequirement(24); var user = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { new Claim("age", "25") })); var context = new AuthorizationHandlerContext(new [] { requirement }, user, resource: null); var handler = new MinAgeHandler(); await handler.HandleAsync(context); Assert.True(context.HasSucceeded); }
Authorization in Razor markup
It is useful to perform the verification of user rights in the markdown when you need to mask UI elements, which a user must not have access to. Surely, you can pass all the necessary trace flags using ViewModel or HttpContext.User directly:
<h4>Age: @User.GetClaimValue("age")</h4>
Also, I would like to add that views are inherited from the RazorPage class, while it is possible to have a direct access to HttpContext via the @Context property.
On the other hand, we can implement IAuthorizationService via DI in the view and check if a user meets the requirements of the necessary policy:
@inject IAuthorizationService AuthorizationService @if (await AuthorizationService.AuthorizeAsync(User, "my-policy"))
I do not recommend using the ignInManager.IsSignedIn(User) call in our test project. The reasons are as follows: we do not use the Microsoft.AspNetCore.Identity authentication library, which contains this class. The method only verifies that a user has the identity with the stored name in the library code.
Permission-based authorization. Custom authorization filter
When authorizing a user, the declarative listing of all requested operations is as follows:
var requirement = OperationListRequirement(new[] { Ops.FooAction, Ops.BarAction }); _authorizationService.AuthorizeAsync(User, resource, requirement);
It is worth using if there is a system of personal permissions in your project. For example, there is a particular set of high-level operations of business logic, as well as users that have manually received rights to the particular operations with the particular resource. The main issue of this approach is that a list of operations easily grows to several hundred even in a small system.
The situation gets simpler if there is no need for the authorization to take into account a specific instance of the protected resource, and our system has a granularity structure to simply assign an attribute with the list of checked operations to the method instead of a hundred of the AuthorizeAsync calls in the protected code.
Using the authorization-based policies [Authorize(Policy = “foo-policy”)] will increase the number of policies in the application. Thus, in this case, it is better to use the role-based authorization. In the code below, a user needs to be a member of all specified roles to get access to FooController:
[Authorize(Roles = "PowerUser")] [Authorize(Roles = "ControlPanelUser")] public class FooController : Controller { }
This solution may not provide a sufficient refinement and flexibility for a system with a huge amount of permissions and their possible combinations. There may be additional issues when it is necessary to use both the role-based and permission-based authorization. Moreover, roles and operations are semantically different things. The solution is to write its own version of the [Authorize]! Attribute. As you can see, it returns the following result:
[AuthorizePermission(Permission.Foo, Permission.Bar)] public IActionResult Edit() { return View(); }
To move on, create enum for operations, as well as requirement and handler – to verify users:
public enum Permission { Foo, Bar } public class PermissionRequirement : IAuthorizationRequirement { public Permission[] Permissions { get; set; } public PermissionRequirement(Permission[] permissions) { Permissions = permissions; } } public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) { //TODO: verification code if the user has rights to perform operations if (requirement.Permissions.Any()) { context.Succeed(requirement); } return Task.CompletedTask; } }
Earlier we discussed that the [Authorize] attribute is a token and it is necessary to use it for AuthorizeFilter. Thus, we will create our own authorization filter. Since action has its own list of permissions, then:
- It is necessary to create a filter instance for each call;
- It is impossible to directly create an instance via the built-in DI-container.
Fortunately, in Core MVC, it is easy to solve these issues using the [TypeFilter] filter:
[TypeFilter(typeof(PermissionFilterV1), new object[] { new[] { Permission.Foo, Permission.Bar } })] public IActionResult Index() { return View(); }
PermissionFilterV1
public class PermissionFilterV1 : Attribute, IAsyncAuthorizationFilter { private readonly IAuthorizationService _authService; private readonly Permission[] _permissions; public PermissionFilterV1(IAuthorizationService authService, Permission[] permissions) { _authService = authService; _permissions = permissions; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { bool ok = await _authService.AuthorizeAsync( context.HttpContext.User, null, new PermissionRequirement(_permissions)); if (!ok) context.Result = new ChallengeResult(); } }
We have received a working solution. Still, it does not look good. To hide details of implementing our filter from the code, we need to use the [AuthorizePermission] attribute:
public class AuthorizePermissionAttribute : TypeFilterAttribute { public AuthorizePermissionAttribute(params Permission[] permissions) : base(typeof(PermissionFilterV2)) { Arguments = new[] { new PermissionRequirement(permissions) }; Order = Int32.MaxValue; } }
Result:
[AuthorizePermission(Permission.Foo, Permission.Bar)] [Authorize(Policy = "foo-policy")] public IActionResult Index() { return View(); }
It should be noted that authorization filters work independently, which allows combining them with each other. It is possible to correct the order of performing our filter in a common queue using the AuthorizePermissionAttribute.Order property.
The authorization review in ASP.NET Core MVC is over. In addition, this information is applicable for WebAPI. If you wish to run these examples, I recommend using the demo project. In my further publication, I will provide the information on how to protect a website and a public API using a dedicated server for the authentication.
Also read:
- Documentation on microsoft.com;
- A series of articles on the NET Core security in the blog of Andrew Lock | .NET Escapades.
Great article
Thanks! You can find more interesting and useful articles written by this author here – http://codingsight.com/author/ilya-chumakov/