Authorization Policies and Middleware in ASP.NET 5

Tuesday, October 6, 2015

Imagine you want to protect a folder full of static assets in the wwwroot directory of an ASP.NET 5 project. There are several different approaches you could take to solve the problem, but here is one flexible solution using authorization policies and middleware.

Services

First, in the Startup class for the application, we will add the required services.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
    });
}

For the default authorization service we’ll make a named policy available, the Authenticated policy. A policy can contain any number of requirements allowing you to check claims and identities. In this code we will ultimately be using the built-in DenyAnonymousAuthorizationRequirement, because this is the type of requirement returned by the RequireAuthenticatedUser method. But again, you could make the requirement verify any number of characteristics about the user and the request.

The name Authenticated is important, because we will refer to this policy when authorizing users for access to a protected folder.

Middleware

Next, let’s write a piece of middleware named ProtectFolder and start with an options class to parameterize the middleware.

public class ProtectFolderOptions
{
    public PathString Path { get; set; }
    public string PolicyName { get; set; }
}

There is also the obligatory extension method to add the middleware to the pipeline.

public static class ProtectFolderExtensions
{
    public static IApplicationBuilder UseProtectFolder(
        this IApplicationBuilder builder, 
        ProtectFolderOptions options)
    {
        return builder.UseMiddleware<ProtectFolder>(options);
    }
}

Then the middlware class itself.

public class ProtectFolder
{
    private readonly RequestDelegate _next;
    private readonly PathString _path;
    private readonly string _policyName;
   
    public ProtectFolder(RequestDelegate next, ProtectFolderOptions options)
    {
        _next = next;
        _path = options.Path;
        _policyName = options.PolicyName;
    }

    public async Task Invoke(HttpContext httpContext, 
                             IAuthorizationService authorizationService)
    {
        if(httpContext.Request.Path.StartsWithSegments(_path))
        {
            var authorized = await authorizationService.AuthorizeAsync(
                                httpContext.User, null, _policyName);
            if (!authorized)
            {
                await httpContext.Authentication.ChallengeAsync();
                return;
            }
        }

        await _next(httpContext);
    }
}

The Invoke method on a middleware object is injectable, so we’ll ask for the current authorization service and use the service to authorize the user if the current request is heading towards a protected folder. If authorization fails we use the authentication manager to challenge the user, which typically redirects the browser to a login page, depending on the authentication options of the application.

Pipeline Configuration

Back in the application’s Startup class, we’ll configure the new middleware to protect the /secret directory with the “Authenticated” policy.

public void Configure(IApplicationBuilder app)
{
    app.UseCookieAuthentication(options =>
    {
        options.AutomaticAuthentication = true;
    });

    app.UseProtectFolder(new ProtectFolderOptions
    {
        Path = "/Secret",
        PolicyName = "Authenticated"
    });

    app.UseStaticFiles();

    // ... more middleware
}

Just make sure the protection middleware is in place before the middleware to serve static files.


Comments
gravatar marcel Wednesday, October 7, 2015
Thanks for posting this but I am getting the following error shown in the browser when starting the project (all code runs fine): Method 'ChallengeAsync' in type 'Microsoft.AspNet.Authentication.Cookies.CookieAuthenticationHandler' from assembly 'Microsoft.AspNet.Authentication.Cookies, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation. Implemented this in an Aurelia/ASP 5 structured app. Using the DNX ** 1.0.0-beta7 ms packages but was prompted to include Microsoft.AspNet.Authentication (1.0.0.-beta5) library (and its **.Cookies) package. What am I doing wrong? Thanks!
gravatar Scott Wednesday, October 7, 2015
@marcel - make sure all the dependencies have the same version (beta 7). My project.json dependencies look like this: https://gist.github.com/OdeToCode/7cc7dceb3c51dc52f2a1
gravatar marcel Wednesday, October 7, 2015
After some messing about with NuGet and manual installs and many external dnu restore commands I managed to get the same dependencies list as yours and all is well.. Many thanks!
gravatar Jason Thursday, October 8, 2015
First, thank you for the article. This popped up during one of my searches yesterday and I had read through it before I realized the source. Your content (here and on Pluaralsight) is always very useful. Now on to the question. I am not quite sure I understand the purpose of configuring authentication and authorization in the ConfigureServices method. I am attempting to construct a piece of custom authentication middleware and the only way I have gotten it to work is to add / configure authorization. By the way, what is the difference between calling AddAuthorization and ConfigureAuthorization? Both seem to accomplish the same thing. As described on my StackOverflow question (http://stackoverflow.com/questions/33002853/custom-authentication-middleware-in-mvc-6), I can only get my middleware to work if I add / configure authorization and then only if I specify the authorization policy that I added / configured on the Authorize attribute. Can you explain what is happening here? Using the default project template, I can throw on the Authorize attribute (without specifying a policy) and it just works. I have not ever had to do this before. I feel like I am missing something as setting up custom middleware didn't seem to be this difficult in the previous version of MVC. Are there any additional resources that you can point me to in order to help guide me through this adventure? Again, thank you for all of the content and any feedback you can provide.
gravatar Jason Thursday, October 8, 2015
In the very short time since my last message, I made some progress (I updated the StackOverflow question). It looks like I can get things working now without having to specify the authorization policy on the Authorization attribute. However, I still don't understand authorization policies and why I might or might not need them. Again, any feedback you can provide will be greatly appreciated.
gravatar Scott Thursday, October 8, 2015
@Jason - the short answer is that authorization policies now encapsulate the rules you want to use when verifying a user has authorization for some action. So, an authorization policy could be anything from "The user is authenticated has the name Jason" to "The day is the closing date of the month, the user is authenticated, is an employee of the company, and has write permissions on the customer account". You can apply these policies by name from an Authorize attribute, making the built-in Authorize attribute more flexible than in the past. The longer answer to this question will need a new post.
gravatar Dave Van den Eynde Tuesday, October 13, 2015
Your "obligatory extension method" doesn't really do anything but call another function. Not that obligatory.
gravatar K. Scott Allen Tuesday, October 13, 2015
@Dave: Obligatory might be a strong word, but once you've worked with the 99.5% of middleware that provides an extension method to a) allow the middleware to be consumed without importing an additional namespace and b) wrap a call to the generic UseMiddleware method with a better name that matches the middleware being consumed, it does feel like a compelling requirement.
gravatar Codenator81 Thursday, October 15, 2015
Try your example but it not work: var authorized = await authorizationService.AuthorizeAsync( httpContext.User, null, _policyName); if (!authorized) { await httpContext.Authentication.ChallengeAsync(); return; } all way not authorized I try var user = await _userManager.FindByIdAsync(httpContext.User.GetUserId()); ; var userRole = await _userManager.IsInRoleAsync(user, _roleName); if (!authorized) { await httpContext.Authentication.ChallengeAsync(); return; } but in httpContext User allways null.
gravatar Codenator81 Thursday, October 15, 2015
Find mistake. Before your middleware need add: app.UseIdentity(); becouse without it no user data in context. So i solve problem and it helps to me. Using dnx version 1.0.0-rc1-15902
Comments are closed.
Pluralsight Courses
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!