a working example of permissions authorization
October 20, 2016Motivation
In my previous post, Practical Permission-based Authorization in ASP.NET Core, I tried to demonstrate how to implement a regime of permissions-based authorization without having to stuff it all into an ever-exploding list of roles, without abusing claims, and without having to roll your own framework-fighting implementation. Resource filters allow you to do this elegantly and still remain in harmony with the framework.
However, I may have been too terse. I got a couple questions and a request for a working example, which seemed like a reasonable request, so that what this post is about. Part of what I realized I needed to do is fill in a bunch of gaps about which I assumed the reader’s a priori knowledge. That was a mistake, and so this post will go into more detail about how all this stuff works.
The code is available at GitHub: trinityrepublic/demo-authorization.
Overview
First things first. In the other post, I linked to the ASP.NET Core documentation, but maybe I didn’t lean into that as aggressively as I should have. If you have not at least read the section called How do filters work?, then you stand a good chance of not following this. Go on, right now. Read it. It will take you 2 minutes, and this post is just a back-button click away.
I’ve used the default web project template from the CLI tooling (1.0.0-preview2-003133
). It uses the ASP.NET Identity
framework to manage authentication (remember, authentication is not the same as authorization).
Permissions are an authorization concern. So we’re going to let Identity handle authentication for us using the defaults.
You’ll see the following in Startup.cs
(with a bunch of stuff omitted…)
namespace WebApplication
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
/* stuff */
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
/* stuff */
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
// ************** THE IMPORTANT PART OF THIS SNIPPET *****************
app.UseIdentity();
// *******************************************************************
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
)
});
}
}
}
The indicated line above, app.UseIdentity()
, is an IApplicationBuilder
extension method provided by the Identity
framework. You can see for yourself what it does, but for our
purposes what you really need to know is that it registers cookie authentication (using similar builder extension methods),
which in turn register CookieAuthenticationMiddleware
, which in turn registers the CookieAuthenticationHandler
, which is
the class that does the actual authentication for us.
This (and any) authentication handler will do the work to figure out if a user’s claims about their identity are valid. If so, then the handler will mark this particular identity as authenticated. So just to be extra clear: whether or not a user is authenticated is always a binary question. Yes, or no. True or false.
Details
Now to authorization. In ASP.NET Core, rather than the old membership system in which users could be placed in roles and
authorization granted based on membership in a role, authorization is built on “policies”. Role membership is a
pre-configured policy and is still supported (see Role based Authorization), but the framework allows you to define
arbitrary policies, which gives us a lot more flexibility in how we authorize our users. But wait! Weren’t we just
talking about authentication? Yes - and here’s the link: the default authorization policy configured in ASP.NET Core is to
test for authentication. That’s why when you first start a new application you slap the [Authorize]
attribute on
everything. Unless you configure other policies, there’s a one-to-one mapping between “is authenticated” and “is
authorized”.
The AuthorizeAttribute
also takes a Policy
parameter that allows you to authorize based on any named policy. In the case
of permissions authorization it should be straightforward to make a “Permissions” policy and specify it like this:
[Authorize(Policy="Permissions")]
public IActionResult Get()
{
/* do get-y stuff in here */
}
Ah…except it’s not that simple. What permissions does a user need? First, let’s back up and see how authorization policies work. Again, the documentation on this topic is really very good, so I strongly encourage you to read it for yourself. However, I will try and summarize here. Policies are implemented via “Requirements”, and with each requirement there must be at least one associated authorization handler. The handlers determine whether or not their associated requirement is satisfied, and then finally the policy is satisfied if any requirement is met.
The tricky part about permissions is that you don’t know what permissions you need to check when the policy is defined, and
polices aren’t parameterizable. All you can do is indicate in the attribute what policy to use, and then an
AuthorizationFilter
is added to that action, and the AuthorizationFilter
evaluates the given policy (by evaluating each
of its requirements until one succeeds or they all fail).
The AuthorizationFilter
makes use of a service that is registered for us when the Identity framework is configured: IAuthorizationService
.
This service allows a bit more control over what things are being evaluated, which is to our advantage. Indeed, if you check out the
Authorizing Within Your Code section in the
documentation, you’ll see that the registered IAuthorizationService
can be injected into controllers just like anything else, which allows
you do evaluate policies and even requirements directly. This is the basis of resource authorization, actually. The good news is that
because the IAuthorizationService
gives us more control, parameterization is no longer an issue:
public IActionResult Get()
{
if (await _authorizationService.AuthorizeAsync(User, context, /* requirement or policy goes here */))
{
return View();
}
else
{
return ChallengeResult();
}
}
Used like this, IAuthorizationService
can clearly allow us to use whatever policy or requirement we want on a
request-by-request basis. Remember though: we wanted to do this in a declarative way using attributes. My solution to this
problem was to use the TypeFilterAttribute
, which gives us access to the DI container. My previous post
explained this technique adequately so I won’t repeat it all here, but here’s the code for the described technique that can
give use a dependency-injected attribute like we wanted.
using System;
using System.Threading.Tasks;
using Microsoft.AspnetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using WebApplication.AspNetCore.Authorization;
namspace WebApplication.AspNetCore.Mvc.Filters
{
public class RequiresPermissionAttribute : TypeFilterAttribute
{
public RequiresPermissionAttribute(params Permission[] permissions)
: base(typeof(RequiresPermissionAttributeImpl))
{
Arguments = new[] { new PermissionAuthorizationRequirement(permissions) };
}
private class RequiresPermissionAttributeImpl : Attribute, IAsyncResourceFilter
{
private readonly ILogger _logger;
private readonly IAuthorizationService _authService;
private readonly PermissionAuthorizationRequirement _permissionRequirement;
public RequiresPermissionAttributeImpl(Ilogger<RequiresPermissionAttribute> logger,
IAuthorizationService authService,
PermissionAuthorizationRequirement permissionRequirement)
{
_logger = logger;
_authService = authService;
_permissionRequirement = permissionRequirement;
}
public async Task OnResourceExecutionAsync(ResourceExecuting context,
ResourceExecutionDelegate next)
{
_logger.LogTrace("Executing RequiresPermissionAttributeImpl filter");
if (!await _authService.AuthorizeAsync(context.HttpContext.User,
context.ActionDescriptor.ToString(),
_permissionRequirement))
{
context.Result = new ChallengeResult();
}
else
{
await next();
}
}
}
}
}
If you read this carefully, you’ll see a couple of types that aren’t explained here.
PermissionAuthorizationRequirement
,
explained in the previous post, and PermissionAuthorizationHandler
, which I will explain here (and also can be
found on GitHub):
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspnetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using WebApplication.AspNetCore.Identity;
using WebApplication.Models;
namespace WebApplication.AspNetCore.Authorization
{
public class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly ILogger _logger;
private readonly DemoUserManager<ApplicationUser> _userManager;
public PermissionAuthorizationHandler(Ilogger<PermissionAuthorizationHandler> logger,
DemoUserManager<ApplicatioNUser> userManager)
{
_logger = logger;
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
var user = await _userManager.GetUserAsync(context.User);
var currentUserPermissions = await _userManager.GetUserPermissionsAsync(user);
var authorized = requirement.RequiredPermissions.AsParallel()
.All(rp => currentUserPermissions.Contains(rp));
if (authorized) context.Succeed(requirement);
}
}
}
There are yet more types here that need explanation, but we’ll be in the weeds really quickly if I try to explain everything
in detail. To summarize, I wrote a custom user manager so that I would be able to load permissions when I get a user from
the identity framework. Once I have a list of the current user’s permissions, it’s trivial to check to see if the required
ones are in the list. If the required permissions are found, then I mark the context as successful for this particular
requirement. This will cause the the call to _authService.AuthorizeAsync(...)
up above in
RequiresPermissionAttributeImpl.OnResourceExecutionAsync
to return true, which means authorization succeeded, and our
resource filter will allow the request to proceed through the action pipeline.
Notes about wiring things up in Startup.cs
One of the tricky things about getting ideas like this to work is connecting the pieces to the framework. It seems very complicated when you’re first learning it, but once you have a handle on it, it won’t seem so complicated.
Registering the custom UserManager for the Identity Framework
In the Startup
class, you’ll need to configure the Identity framework to use the custom UserManager
. In this case, it’s
done in ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddIdentity<ApplicationUser>, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddUserManager<DemoUserManager<ApplicationUser>>() // <--- right here!
.AddDefaultTokenProvider();
// ...
}
Registering handlers for the custom authorization requirement
Just becauase you write a custom authorization requirement class doesn’t mean much of anything, even if you reference it in
the invocation to IAuthorizationService.AuthorizeAsync()
. You have to register a handler for it in the dependency
injection container so the IAuthorizationService
can look it up. It matches the requirement types for you, so all you have
to do is register the handler like anything else:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
// ...
}
If you’ll remember from above, the handler inherits from AuthorizationHandler<PermissionAuthorizationRequirement>
. It has
a generic type that matches the type of the requirement the handler handles, and the authorization service will match
handlers based on that generic type.
Wrap up
Of course, at this point the attribute can be used on a controller action:
[Authorize]
public class MyController : Controller
{
[RequiresPermission(Permissions.APermissionOfSomeKind)]
public IActionResult SomeAction()
{
// stuff
}
}
I hope this clears up some gaps I left in the last post, and certainly check out my sample code: trinityrepublic/demo-authorization;