Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Introduction to Authorisation in ASP.NET Core

$
0
0

This is the next in series of posts about authentication and authorisation in ASP.NET Core. In the first post we introduced authentication in ASP.NET Core at a high level, introducing the concept of claims-based authentication. In the next two post, we looked in greater depth at the Cookie and JWT middleware implementations to get a deeper understanding of the authentication process. Finally, we looked at using OAuth 2.0 and OpenID Connect in your ASP.NET Core applications.

In this post we'll learn about the authorisation aspect of ASP.NET Core.

Introduction to Authorisation

Just to recap, authorisation is the process of determining if a given user has the necessary attributes/permissions to access a given resource/section of code. In ASP.NET Core, the user is specified by a ClaimsPrincipal object, which may have one or more associated ClaimsIdentity, which in turn may have any number of Claims. The process of creating the ClaimsPrincipal and assigning it the correct Claims is the process of authentication. Authentication is independent and distinct from authorisation, but must occur before authorisation can take place.

In ASP.NET Core, authorisation can be granted based on a number of different factors. These may be based on the roles of the current user (as was common in previous version of .NET), the claims of the current user, the properties of the resource being accessed, or any other property you to care to think of. In this post we'll cover some of the most common approaches to authorising users in your MVC application.

Authorisation in MVC

Authorisation in MVC all centres around the AuthorizeAttribute. In it's simplest form, applying it to an Action (or controller, or globally) marks that action as requiring an authenticated user. Thinking in terms of ClaimsPrincipal and ClaimsIdentity, that means that the current principal must contain a ClaimsIdentity for which IsAuthenticated=true.

This is the coarsest level of granularity - either you are authenticated, and you have access to the resource, or you aren't, and you do not.

You can use the AllowAnonymousAttribute to ignore an AuthorizeAttribute, so in the following example, only authorised users can call the Manage method, while anyone can call the Logout method:

[Authorize]
public class AccountController: Controller
{
    public IActionResult Manage()
    {
        return View();
    }

    [AllowAnonymous]
    public IActionResult Logout()
    {
        return View();
    }
}

Under the hood

Before we go any further I'd like to take a minute to dig into what is actually happening under the covers here.

The AuthorizeAttribute applied to your actions and controllers is mostly just a marker attribute, it does not contain any behaviour. Instead, it is the AuthorizeFilter which MVC adds to its filter pipeline when it spots the AuthorizeAttribute applied to an action. This filter implements IAsyncAuthorizationFilter, so that it is called early in the MVC pipeline to verify the request is authorised:

public interface IAsyncAuthorizationFilter : IFilterMetadata
{
    Task OnAuthorizationAsync(AuthorizationFilterContext context);
}

AuthorizeFilter.OnAuthorizationAsync is called to authorise the request, which undertakes a number of actions. The method is reproduced below with some precondition checks removed for brevity - we'll dissect it in a minute:

public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
    var effectivePolicy = Policy;
    if (effectivePolicy == null)
    {
        effectivePolicy = await AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData);
    }

    if (effectivePolicy == null)
    {
        return;
    }

    // Build a ClaimsPrincipal with the Policy's required authentication types
    if (effectivePolicy.AuthenticationSchemes != null && effectivePolicy.AuthenticationSchemes.Count > 0)
    {
        ClaimsPrincipal newPrincipal = null;
        for (var i = 0; i < effectivePolicy.AuthenticationSchemes.Count; i++)
        {
            var scheme = effectivePolicy.AuthenticationSchemes[i];
            var result = await context.HttpContext.Authentication.AuthenticateAsync(scheme);
            if (result != null)
            {
                newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result);
            }
        }
        // If all schemes failed authentication, provide a default identity anyways
        if (newPrincipal == null)
        {
            newPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
        }
        context.HttpContext.User = newPrincipal;
    }

    // Allow Anonymous skips all authorization
    if (context.Filters.Any(item => item is IAllowAnonymousFilter))
    {
        return;
    }

    var httpContext = context.HttpContext;
    var authService = httpContext.RequestServices.GetRequiredService<IAuthorizationService>();

    // Note: Default Anonymous User is new ClaimsPrincipal(new ClaimsIdentity())
    if (!await authService.AuthorizeAsync(httpContext.User, context, effectivePolicy))
    {
        context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());
    }
}

First, it calculates the applicable AuthorizationPolicy for the request. This sets the requirements that must be met for the request to be authorised. The next step is to attempt to authenticate the request by calling AuthenticateAsync(scheme) on the AuthenticationManager found at HttpContext.Authentication. This will run through the authentication process I have discussed in previous posts, and if successful, returns an authenticated ClaimsPrincipal back to the filter.

Once an authenticated principal has been obtained, the authorisation process can begin. First, the method is checked to see if it has an IAllowAnonymousFilter applied (added when an AllowAnonymousAttribute is used), and if it does, returns successfully without any further processing.

If authorisation is required, then the filter requests an instance of IAuthorizationService from the HttpContext. This service neatly encapsulates all the logic for deciding whether a ClaimsPrincipal meets the requirements of the particular AuthorizationPolicy. A call to IAuthorizationService.AuthorizeAsync() returns a boolean, indicating if the result was successful.

If the IAuthorizationService indicates the user was not successful, the AuthorizationFilter returns a ChallengeResult, bypassing the remainder of the MVC pipeline. When executed, this result calls ChallengeAsync on the AuthenticationManager, which in turn calls HandleUnauthorizedAsync or HandleForbiddenAsync on the underlying AuthenticationHandler as covered previously.

The end result will be either a 403 indicating the user does not have permission, or a 401 indicating they are not logged in, which will generally be captured and converted to a redirect to the login page.

The details of how the AuthorizationFilter works is rather tangential to this introduction in general, but it highlights the separation of concerns and abstractions used to facilitate easier testing, and the use of dumb marker attributes to act has hooks for other more complex services.

Authorising based on claims

Now that detour is over and we understand more of how authorisation works in MVC, we can look at creating some specific authorisation requirements, more than just 'you logged in'.

As I discussed in the introduction to authentication, identity in ASP.NET Core is really entirely focussed around Claims. Given that fact, one of the most obvious modes of authentication is to check that a user has a given claim. For example, there may be a section of your site which is only available to VIPs. In order to authorise requests you could create a CanAccessVIPArea policy, or more specifically an AuthorizationPolicy.

To create a new policy, we configure them as part of the service configuration in the ConfigureServices method of your Startup class using an AuthorizationPolicyBuilder. We provide a name for the policy, "CanAccessVIPArea", and add a requirement that the user has the VIPNumber claim:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "CanAccessVIPArea",
            policyBuilder => policyBuilder.RequireClaim("VIPNumber"));
    });
}

This requirement ensures only that the ClaimsPrincipal has the VIPNumber claim, it does not make any requirements on the value of the claim. If we required the claim to have specific values, we can pass those to the RequireClaimMethod:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "CanAccessVIPArea",
            policyBuilder => policyBuilder.RequireClaim("VIPNumber", "1", "2"));
    });
}

With our policy configured, we can now apply it to our actions or controllers to protect them from the proletariat:

[Authorize(Policy = "CanAccessVIPArea")]
public class ImportantController: Controller
{
    public IActionResult FancyMethod()
    {
        return View();
    }
}

Note that if you have multiple AuthorizeAttributes applied to an action then all of the policies much be satisfied for the request to be authorised.

Authorising based on roles

Before claims based authentication was embraced, authorisation by role was a common approach. As shown previously, ClaimsPrincipal still has an IsInRole(string role) method that you can use if needed. In particular, you can specify required roles on AuthorizeAttributes, which will then verify the user is in the correct role before authorising the user:

[Authorize(Roles = "HRManager, CEO")]
public class AccountController: Controller
{
    public IActionResult ViewUsers()
    {
        return View();
    }
}

However, other than for simplicity in porting from ASP.NET 4.X, I wouldn't recommend using the Roles property on the AuthorizeAttribute. Instead, it is far better to use the same AuthorizationPolicy infrastructure as for Claim requirements. This provides far more flexibility than the previous approach, making it simpler to update when policies change, or they need to be dynamically loaded for example.

Configuring a role-based policy is much the same as for Claims and allows you to specify multiple roles; membership in any of these will satisfy the policy requirement.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthorization(options =>
    {
        options.AddPolicy(
            "CanViewAllUsers",
            policy => policy. RequireRole("HRManager", "CEO"));
    });
}

We can now update the previous method to use our new policy:

[Authorize(Policy = "CanViewAllUsers")]
public class AccountController: Controller
{
    public IActionResult ViewUsers()
    {
        return View();
    }
}

Later on, if we decide to take a claims based approach to our authorisation, we can just update the policies as appropriate, rather than having to hunt through all the Controllers in our solution to find usages of the magic role strings.

Behind the scenes, the roles of a ClaimsPrincipal are actually just claims create with a type of ClaimsIdentity.RoleClaimType. By default, this is given by ClaimType.Role, which is the string http://schemas.microsoft.com/ws/2008/06/identity/claims/role. When a user is authenticated appropriate claims are added for their roles which can be found later as required.

It's worth bearing this in mind if you have difficult with AuthorizeAttributes not working. Most external identity providers will use a different set of claims representing role, name etc that do not marry up with the values used by Microsoft in the ClaimType class. As Dominick Baier discusses on his blog, this can lead to situations where claims are not translated and so users can appear to not be in a given role. If you run into issues where your authorisation does not appear to working correctly, I strongly recommend you check out his post for all the details.

Generally speaking, unless you have legacy requirements, I would recommend against using roles - they are essentially just a subset of the Claims approach, and provide limited additional value.

Summary

This post provided an introduction to authorisation in ASP.NET Core MVC, using the AuthorizeAttribute. We touched on three simple ways you can authorise users - based on whether they are authenticated, by policy, and by role. We also went under the covers briefly to see how the AuthorisationFilter works when called as part of the MVC pipeline.

In the next post we will explore policies further, looking at how you can create custom policies and custom requirements.


Viewing all articles
Browse latest Browse all 743

Trending Articles