Authentication In An ASP.NET Core API - Part 1: Identity, Access Denied

In this series, I am going to outline some basic approaches to authenticating your .NET Core API using either ASP.NET Core Identity or token-based authentication with a JSON Web Token (JWT). I will also explore how to configure your application to return proper response types to both Redirect To Login and Redirect To Access Denied events when using ASP.NET Core Identity.

Authentication In A Dot Net Core API

  1. ASP.NET Core Identity, Accessed Denied.
  2. ASP.NET Core Identity, Accessed Granted.
  3. Token based authentication with a JSON Web Token (JWT).
  4. Migrating to ASP.NET Core 2.0. (Coming Soon)

Setup

To demonstrate these concepts of authentication, I will use the Pioneer Code Blog source code to illustrate how I secured API endpoints that allow for CRUD operations against the supporting tables in its database. That being said, let's take look at the file structure and identify where the API controllers exist today.

Solution Explorer Area API Endpoints

As you can see, I created an Area to house all my operations that are related to the administration of my site. Notably, those that allow me to managed my blog posts. In the Area titled "Admin" lives a collection of API Controllers that correspond to individual tables (entities) in the database.

For more information about creating Areas, you can visit my blog post entitled Creating Areas In ASP.NET MVC Core.

To keep things simple lets focus on a single endpoint that lives in a single API controller.

[Route("api/categories")]
public class CategoryApiController : Controller
{
    private readonly ICategoryService _categoryService;

    public CategoryApiController(ICategoryService categoryService)
    {
        _categoryService = categoryService;
    }

    [HttpGet]
    public IEnumerable<Category> GetAll(int? count, int? page)
    {
        if (count == null || page == null)
        {
            return _categoryService.GetAll();
        }

        return _categoryService.GetAllPaged((int)count, (int)page);
    }
}

Here you can see an endpoint the maps to a GET request on the api/categories endpoint. If we make a request to this endpoint, we will get a collection of categories with a Response of 200 OK.

Unauthenticated Endpoint

ASP.NET Identity

ASP.NET Core Identity is a membership system which allows you to add login functionality to your application. Users can create an account and login with a username and password or they can use an external login providers such as Facebook, Google, Microsoft Account, Twitter and more. ASP.NET Core Identity

We will start out securing our API endpoints by introducing ASP.NET Core Identity into our application. If you are working on an MVC project, Identity is available in the Microsoft.AspNetCore.Builder lib. Of which, is scaffold in just about every MVC project you are creating. On the other hand, if you are creating an API without the "typical" MVC lib project dependencies, you might have to add a reference to <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink.Loader" Version="14.1.0" /> in your csproj file.

Setting up DbContext

Before we configure our Identity service, we first must ensure our database is in order. To do this, I needed to implement my existing Blog Context as a derived class of IdentityDbContext<UserEntity>. I set the type as UserEntity, which itself is a derived class of IdentityUser. Once the context was setup, I ran my Entity Framework migrations to scaffold the necessary tables.

In short, this now ensures my db context has all the necessary configuration needed for Identity and my UserEntity shares all the base types provided by IdentityUser while being expendable to suite any future needs.

Configuring Identity

Once our data store is in order, we can now add and configure our Identity Service. In the Startup.ConfigServices(IServiceCollection services) function, I added the following.

services.AddIdentity<UserEntity, IdentityRole>()
    .AddEntityFrameworkStores<BlogContext>();

This effectively adds Identity to the Pioneer Blog services collection.

Next, we need to tell our application to use Identity. This need to be done before we add MVC in order to apply authentication to our MVC calls do to order of operation concerns.

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory,
    IdentitySetup identitySetup)
{
    // Shortened.....
    app.UseIdentity();
    // Add MVC
}

Authorization Attribute

In order to tell Identity that we don't want to allow unauthorized user access to our endpoint, we need to add the Authorization Attribute to either a class or directly on a method. This depends on the scope you want to apply to your authorization plan. In my case, I will apply this attribute to a backing service method that supports my GET Categories API Controller endpoint.

[Authorize]
[HttpGet]
public IEnumerable<Category> GetAll(int? count, int? page)
{
    if (count == null || page == null)
    {
        return _categoryService.GetAll();
    }

    return _categoryService.GetAllPaged((int)count, (int)page);
}

Let's try hitting our API endpoint again.

Fail and Redirect

This time around we are not getting any Categories back in the Response. Which is great, this is what we wanted. But what about our Status Code being 200 OK and the Response payload returning HTML? This happened because Identity by default is configured to redirect an unauthenticated request to a login in page.

This obviously does not suit our APIs needs. That being said, let's configure our IdentityOptions to take in account cases where we do not want to redirect to login and instead we want to return a 401 Unauthorized response. In the Startup.ConfigServices(IServiceCollection services) method add the following.

services.Configure<IdentityOptions>(config =>
{
    config.Cookies.ApplicationCookie.Events =
        new CookieAuthenticationEvents
        {
            OnRedirectToLogin = ctx =>
            {
                if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
                {
                    ctx.Response.StatusCode = 401;
                    return Task.FromResult<object>(null);
                }

                ctx.Response.Redirect(ctx.RedirectUri);
                return Task.FromResult<object>(null);
            }
        };
});

In the IdentityOptions configuration we are setting up a way to intercept an Event, of which is the OnRedirectToLogin Event. When this Event is raised, we first check our URL segment to see if it starts with "/api". If it does and the response is of type 200 OK, we know we have run into the exact issue identified above. This time, we will return immediately with a status code of 401 Unauthorized.

401 Identity

That is it! We now have a way to block access to API endpoints of our choosing using Identity. In the next part of this series, I will talk about adding a way to register a user using Identity and allowing access to API endpoints that require authentication.