ASP.NET Core middleware and authorization

I was prompted to write this post by this question. In general, the question is about using ASP.NET Core built-in authorization to restrict access to a middleware. In ASP.NET Core the authorization mechanism is well exposed for MVC (through AuthorizeAttribute), but for middleware it's a manual job (at least for now). The reason for that might be the fact that there is no too many terminal middleware.

This was not the first time I've received this question, so I've quickly responded with typical code to achieve the task. But, after some thinking, I've decided I will put a detailed answer here.

Policy-based authorization

At its core, the authorization in ASP.NET Core is based on policies. Other available ways of specifying requirements (roles, claims) are in the end evaluated to policies. This means that it is enough to be able to validate a policy for the current user. This can be easily done with help of IAuthorizationService. All one needs is a policy name and HttpContext. Following authorization middleware gets the job done.

public class AuthorizationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _policyName;

    public AuthorizationMiddleware(RequestDelegate next, string policyName)
    {
        _next = next;
        _policyName = policyName;
    }

    public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService)
    {
        AuthorizationResult authorizationResult =
            await authorizationService.AuthorizeAsync(httpContext.User, null, _policyName);

        if (!authorizationResult.Succeeded)
        {
            await httpContext.ChallengeAsync();
            return;
        }

        await _next(httpContext);
    }
}

Of course, middleware registration can be encapsulated in an extensions method for easier use.

public static class AuthorizationApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app, string policyName)
    {
        // Null checks removed for brevity
        ...

        return app.UseMiddleware(policyName);
    }
}

The only thing left is to put this middleware in front of middleware which should have restricted access (it can be placed multiple times if multiple policies need to be validated).

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...

        services.AddAuthorization(options =>
        {
            options.AddPolicy("PolicyName", ...);
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...

        app.UseAuthentication();

        app.Map("/policy-based-authorization", branchedApp =>
        {
            branchedApp.UseAuthorization("PolicyName");

            ...
        });

        ...
    }
}

Simple and effective. Goal achieved, right?

Simple authorization, roles and schemes

Despite being my go-to solution, the above approach is far from perfect. It doesn't expose full capabilities and is not user-friendly. Something more similar to AuthorizeAttribute would be a lot better. This means making full use of policies, roles, and schemes. At first, this might sound like some serious work, but the truth is that all the hard work is done for us, we just need to go beyond Microsoft.AspNetCore.Authorization and use some services from Microsoft.AspNetCore.Authorization.Policy package. But before that can be done, a user-friendly way of defining restrictions is needed. This is no challenge, as ASP.NET Core has an interface for that.

internal class AuthorizationOptions : IAuthorizeData
{
    public string Policy { get; set; }

    public string Roles { get; set; }

    public string AuthenticationSchemes { get; set; }
}

This options class is very similar to AuthorizeAttribute. This isn't a surprise as AuthorizeAttribute also implements IAuthorizeData.

Implementing IAuthorizeData allows class to be transformed into AuthorizationPolicy with help of IAuthorizationPolicyProvider.

public class AuthorizationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IAuthorizeData[] _authorizeData;
    private readonly IAuthorizationPolicyProvider _policyProvider;
    private AuthorizationPolicy _authorizationPolicy;

    public AuthorizationMiddleware(RequestDelegate next,
        IAuthorizationPolicyProvider policyProvider,
        IOptions authorizationOptions)
    {
        // Null checks removed for brevity
        _next = next;
        _authorizeData = new[] { authorizationOptions.Value };
        _policyProvider = policyProvider;
    }

    public async Task Invoke(HttpContext httpContext, IPolicyEvaluator policyEvaluator)
    {
        if (_authorizationPolicy is null)
        {
            _authorizationPolicy =
                await AuthorizationPolicy.CombineAsync(_policyProvider, _authorizeData);
        }

        ...

        await _next(httpContext);
    }

    ...
}

The policy needs to be evaluated. This requires two calls to IPolicyEvaluator, one for authentication and one for authorization.

public class AuthorizationMiddleware
{
    ...

    public async Task Invoke(HttpContext httpContext, IPolicyEvaluator policyEvaluator)
    {
        ...

        AuthenticateResult authenticateResult =
            await policyEvaluator.AuthenticateAsync(_authorizationPolicy, httpContext);
        PolicyAuthorizationResult authorizeResult =
            await policyEvaluator.AuthorizeAsync(_authorizationPolicy, authenticateResult, httpContext, null);

        if (authorizeResult.Challenged)
        {
            await ChallengeAsync(httpContext);
            return;
        }
        else if (authorizeResult.Forbidden)
        {
            await ForbidAsync(httpContext);
            return;
        }

        await _next(httpContext);
    }

    ...
}

The last thing is handling Challenged and Forbidden scenarios. There are ready to use HttpContext extension methods which do that, but it's important to remember to make use of schemes if they have been provided.

public class AuthorizationMiddleware
{
    ...

    private async Task ChallengeAsync(HttpContext httpContext)
    {
        if (_authorizationPolicy.AuthenticationSchemes.Count > 0)
        {
            foreach (string authenticationScheme in _authorizationPolicy.AuthenticationSchemes)
            {
                await httpContext.ChallengeAsync(authenticationScheme);
            }
        }
        else
        {
            await httpContext.ChallengeAsync();
        }
    }

    private async Task ForbidAsync(HttpContext httpContext)
    {
        if (_authorizationPolicy.AuthenticationSchemes.Count > 0)
        {
            foreach (string authenticationScheme in _authorizationPolicy.AuthenticationSchemes)
            {
                await httpContext.ForbidAsync(authenticationScheme);
            }
        }
        else
        {
            await httpContext.ForbidAsync();
        }
    }
}

Now the registration method can be modified. An important thing to note here is that not setting any of the AuthorizationOptions properties will result in using default policy (same as decorating action or controller with [Authorize]). This case might be worth an overload.

public static class AuthorizationApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
    {
        return app.UseAuthorization(new AuthorizationOptions());
    }

    public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app,
        AuthorizationOptions authorizationOptions)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (authorizationOptions == null)
        {
            throw new ArgumentNullException(nameof(authorizationOptions));
        }

        return app.UseMiddleware<AuthorizationMiddleware>(Options.Create(authorizationOptions));
    }
}

This makes all capabilities provided by AuthorizeAttribute available to middleware pipeline. If the application is not using MVC it's important to remember about adding policy services.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...

        services.AddAuthorization(options =>
        {
            options.AddPolicy("PolicyName", ...);
        })
        .AddAuthorizationPolicyEvaluator();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...

        app.UseAuthentication();

        app.Map("/simple-authorization", branchedApp =>
        {
            branchedApp.UseAuthorization();

            ...
        });

        app.Map("/role-based-authorization", branchedApp =>
        {
            branchedApp.UseAuthorization(new AuthorizationOptions { Roles = "Employee" });

            ...
        });

        app.Map("/policy-based-authorization", branchedApp =>
        {
            branchedApp.UseAuthorization(new AuthorizationOptions { Policy = "EmployeeOnly" });

            ...
        });

        ...
    }
}

All the code above is a copy-paste solution when one wants to restrict middleware from outside, but it can also be easily adapted to put inside a middleware (which in the end I decided to do in case of my Server-Sent Events middleware).

Small note about the future

The state of authorization in the middleware pipeline should be expected to change. ASP.NET Core 3.0 is supposed to make Endpoint Routing available outside of MVC and it comes with support for authorization. In ASP.NET Core 2.2 there is already an authorization middleware (quite similar to the one above) which restricts endpoints based on IAuthorizeData from metadata. This means that in 3.0 it may be possible to define a restricted endpoint pointing to a middleware.