Tackle more complex security policies for your ASP.NET Core app

March 12, 2018 · 5 minute read · Tags: core | security

If it’s Tuesday and it’s raining, only permit people with umbrellas to enter the lobby…

Your boss

Some days you can get away with locking down parts of your app to “admins”, other times you’re going to get more complicated authorization requirements to deal with.

In the last post we saw how we could require users to have completed basic training in order to access certain functions within our app.

Now we’ll tackle a more complex requirement; staff must complete at least 3 months service (and pass basic training) before they’re let loose with customer accounts.

When simple claims are not enough

Last time we simply checked for the existence of a claim.

This time we’re going to need a little more logic if we’re to confirm that they’ve been employed for 3 months or more.

We can use a simple DataTime, stored as a claim to record when a user started working for the company.

var claims = new[]
{
    new Claim(ClaimTypes.Name, request.Username),
    new Claim("CompletedBasicTraining", ""),

    new Claim(CustomClaimTypes.EmploymentCommenced, 
                new DateTime(2017,12,1).ToString(),
                ClaimValueTypes.DateTime)
};

Again it’s worth noting (as in previous examples) this is more likely to come from a database in a real app, not hardcoded as in our example.

You might notice that the magic string we’ve used in previous claims e.g. “CompletedBasicTraining” has gone in this case, to be replaced by a reference to CustomClaimTypes.EmploymentCommenced.

This simple class saves us from relying on magic strings which can be easily mistyped (and gives us one place to see what claims our app defines).

public static class CustomClaimTypes
{
    public const string EmploymentCommenced = "EmploymentCommenced";
}

Custom policies

Now to the actual logic needed to enforce our policy.

To put your own logic around authorization like this you’ll need to create your own authorization handler.

First up you need a requirement.

public class MinimumMonthsEmployedRequirement : IAuthorizationRequirement
{
    public int MinimumMonthsEmployed { get; private set; }

    public MinimumMonthsEmployedRequirement(int minimumMonths)
    {
        this.MinimumMonthsEmployed = minimumMonths;
    }
}

We can pass any data we like into a requirement and store it as fields.

This requirement will be passed to a handler by ASP.NET Core when our policy is invoked.

Here’s the handler for our MinimumMonthsEmployedRequirement.

public class MinimumMonthsEmployedHandler 
    : AuthorizationHandler<MinimumMonthsEmployedRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        MinimumMonthsEmployedRequirement requirement)
    {
        var employmentCommenced = context.User
            .FindFirst(claim => claim.Type == CustomClaimTypes.EmploymentCommenced).Value;

        var employmentStarted = Convert.ToDateTime(employmentCommenced);
        var today = LocalDate.FromDateTime(DateTime.Now);

        var monthsPassed = Period
            .Between(employmentStarted.ToLocalDateTime(), today.AtMidnight(), PeriodUnits.Months)
            .Months;

        if (monthsPassed >= requirement.MinimumMonthsEmployed)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

ASP.NET Core authorization handlers automatically provide access to the current HttpContext, which is handy.

This means we have full access to the user and their claims and can use this to retrieve the relevant claim(s) to conduct our auth logic check.

This example uses the handy dandy NodaTime library to figure out whether the number of months between the employment commencement date and today is greater than the MinimumMonthsEmployed (as defined by our requirement).

We only call context.Succeed(requirement) if the logic checks out and we’re sure that the user has been employed for the requisite period.

If we don’t call context.Succeed then the requirement will be considered unmet, and the user will be denied access.

I’ve got a policy and I’m not afraid to use it

Wiring this up requires a simple tweak in Startup.cs, in the ConfigureServices method.

services.AddAuthorization(options =>
{
    options.AddPolicy("TrainedStaffOnly",
        policy => policy
        .RequireClaim("CompletedBasicTraining")
        .AddRequirements(new MinimumMonthsEmployedRequirement(3)));
});

Now our users need to have completed basic training and have been employed for 3 months (minimum) to perform any actions we decorate with the TrainedStaffOnly attribute.

[Authorize(Policy = "TrainedStaffOnly")]
[HttpPost("DeleteUser")]
public IActionResult DeleteUser(string username)
{
    // go wild, delete the user, do what you have to...
    return Ok("Deleted");
}

Before this works though, there is one more step.

Right now, Core has no idea about our handler for MinimumMonthsEmployedRequirement.

For Core to locate any of our authorization handlers we need to register them with Core’s dependency injection framework.

We can do this easily enough in the ConfigureServices method…

// add this...
services.AddSingleton<IAuthorizationHandler, MinimumMonthsEmployedHandler>();

// before this lot...
services.AddAuthorization(options =>
{
    options.AddPolicy("TrainedStaffOnly",
        policy => policy
        .RequireClaim("CompletedBasicTraining")
        .AddRequirements(new MinimumMonthsEmployedRequirement(3)));
});

Testing our work

As you employ more complex requirements like this one, so the need for unit tests increases.

Testing this manually can prove a little tricky, especially where dates are involved.

Thankfully it’s fairly simple to write unit tests for custom auth handlers.

[Fact]
public void EmployeeWithRequiredMonthsServiceIsAuthorized()
{
    var employmentCommenced = DateTime.Now.AddMonths(-3).AddDays(-1).ToString();

    var user = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> {
        new Claim(CustomClaimTypes.EmploymentCommenced, employmentCommenced, ClaimValueTypes.DateTime)
    }));

    var requirement = new MinimumMonthsEmployedRequirement(3);
    var authorizationHandler = new MinimumMonthsEmployedHandler();

    var handlers = new List<IAuthorizationRequirement> { requirement };
    var context = new AuthorizationHandlerContext(handlers, user, null);
    authorizationHandler.HandleAsync(context);

    Assert.True(context.HasSucceeded);
}

Here we make sure the employment commencement date is at least 3 months ago.

We can simulate our user by creating a new ClaimsPrincipal and assigning the “employment commenced date” claim.

Then we create instances of our requirement and handler and spin up a new AuthorizationHandlerContext which brings our handler and user together.

Finally it’s a simple task to call the handler (passing in the context we just created) before checking that the user’s claims stood up to scrutiny and successfully passed the auth requirement.

Concentrate on your logic

Custom authentication handlers and policies are easy to spin up in ASP.NET Core which means you can focus more of your effort on the actual logic you need to implement and not the plumbing that goes with it.

photo credit: Leonegraph Welcome to umbrella city… via photopin (license)

Next up

But, which flavor of ASP.NET?
Blazor, Razor, MVC… what does it all mean?!
There’s a buzz about Blazor - Here’s why
People are talking about Blazor but will it ever really replace JS?
Starting out with the ASP.NET Core React template (part 3 - Separating out the frontend)
How to run the frontend and backend parts of the ASP.NET Core React project template separately