RESTful API with authentication using Web API and JWT - Part 1

Introduction

We will be creating a RESTful (REST like) HTTP service using Web API feature of the ASP.NET framework.

The purpose of this code is to develop the Restaurent API, using Microsoft Web API with (C#),which authenticates and authorizes some requests, exposes OAuth2 endpoints, and returns data about meals and reviews for consumption by the caller. The caller in this case will be Postman, a useful utility for querying API’s.

Note: Anytime you are stuck you can directly download the code from here on GitHub or ask in the comments below.

Pre-requisite

  1. Basic knowledge Web API with C#
  2. Visual Studio
  3. Postman

Project Template

  • Open Visual Studio. I’m using VS 2017 Community Edition, you can use any version you have
  • Create a new Project with the following configuration

Create New Project

Project Template

Create New Project

Install Packages

Run the collowing commands in the package manager console

Open NuGet Package Manager

install-package EntityFramework
install-package Microsoft.AspNet.Cors
install-package Microsoft.AspNet.Identity.Core
install-package Microsoft.AspNet.Identity.EntityFramework
install-package Microsoft.AspNet.Identity.Owin
install-package Microsoft.AspNet.WebApi.Cors
install-package Microsoft.AspNet.WebApi.Owin
install-package Microsoft.Owin.Cors
install-package Microsoft.Owin.Security.Jwt
install-package Microsoft.Owin.Host.SystemWeb
install-package System.IdentityModel.Tokens.Jwt
install-package Thinktecture.IdentityModel.Core

These are the minimum number of packages required to provide data persistence, enable CORS (Cross-Origin Resource Sharing), and enable generating and autAhenticating/authorizing with JWT.

Entity Framework Setup

We will be using Entity Framework for data persistence. Entity Framework will take care of generating a database, adding tables, stored procedures and so on. As an added benefit, Entity Framework will also upgrade the schema automatically as we make changes.

Create a new IdentityDbContext called MealsContext, which will give us Users, Roles and Claims in our database. Add this under a folder called Core, for organization. We will add our entities to this later.

namespace Meals.Service.Core
{
    using Microsoft.AspNet.Identity.EntityFramework;

    public class MealsContext : IdentityDbContext
    {
    }
}

Claims are used to describe useful information that the user has associated with them. We will use claims to tell the client which roles the user has. The benefit of roles is that we can prevent access to certain methods/controllers to a specific group of users, and permit access to others.

Add a DbMigrationsConfiguration class and allow automatic migrations, but prevent automatic data loss

namespace Meals.Service.Core
{
    using System.Data.Entity.Migrations;

    public class Configuration : DbMigrationsConfiguration<MealsContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = false;
        }
    }
}

Now tell Entity Framework how to update the database schema using an initializer, as follows;

namespace Meals.Service.Core
{
    using System.Data.Entity;

    public class Initializer : MigrateDatabaseToLatestVersion<MealsContext, Configuration>
    {
    }
}

This tells Entity Framework to go ahead and upgrade the database to the latest version automatically for us.

Finally, tell your application about the initializer by updating the Global.asax.cs file as follows;

Also we will configure our application to return camel-case JSON (thisIsCamelCase), instead of the default pascal-case (ThisIsPascalCase).

namespace Meals.Service
{
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using System.Data.Entity;
    using System.Web.Http;

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
            Database.SetInitializer(new Initializer());
            var formatters = GlobalConfiguration.Configuration.Formatters;
            var jsonFormatter = formatters.JsonFormatter;
            var settings = jsonFormatter.SerializerSettings;
            settings.Formatting = Formatting.Indented;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}

Data Provider

By default, Entity Framework will configure itself to use LocalDB. If this is not desirable, say you want to use SQL Express instead, you need to make the following adjustments;

Open the Web.config file and delete the following code

<entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
        <parameters>
            <parameter value="mssqllocaldb" />
        </parameters>
    </defaultConnectionFactory>
    <providers>
        <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
</entityFramework>

And add the connection string

<connectionStrings>
    <add name="BooksContext" providerName="System.Data.SqlClient" connectionString="Server=.;Database=Books;Trusted_Connection=True;" />
</connectionStrings>

Now we’re using SQL Server directly rather than LocalDB.

CORS (Cross-Origin Resource Sharing)

This step is completely optional. We are adding in CORS support here because when we come to write our client app we will likely use a separate HTTP server (for testing and debugging purposes). When released to production, these two apps would use the same host (Internet Information Services (IIS)).

To enable CORS, open WebApiConfig.cs and add the following code to the beginning of the Register method

var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
config.MessageHandlers.Add(new PreflightRequestsHandler());

Now create a new class in App_Start folder

namespace Meals.Service
{
    using System.Net;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;

    public class PreflightRequestsHandler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request.Headers.Contains("Origin") && request.Method.Method == "OPTIONS")
            {
                var response = new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
                response.Headers.Add("Access-Control-Allow-Origin", "*");
                response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization");
                response.Headers.Add("Access-Control-Allow-Methods", "*");
                var tsc = new TaskCompletionSource<HttpResponseMessage>();
                tsc.SetResult(response);
                return tsc.Task;
            }
            return base.SendAsync(request, cancellationToken);
        }
    }
}

In the CORS workflow, before sending a DELETE, PUT or POST request, the client sends an OPTIONS request to check that the domain from which the request originates is the same as the server. If the request domain and server domain are not the same, then the server must include various access headers that describe which domains have access. To enable access to all domains, we just respond with an origin header (Access-Control-Allow-Origin) with an asterisk to enable access for all.

The Access-Control-Allow-Headers header describes which headers the API can accept/is expecting to receive. The Access-Control-Allow-Methods header describes which HTTP verbs are supported/permitted.

Data Model

The API will expose meals, and meals will have reviews.

Under the Models folder add a new class called Meal. Add the following code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Meals.Service.Models
{
    using System.Collections.Generic;

    public class Meal
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public string ImageUrl { get; set; }
        public virtual List<Review> Reviews { get; set; }
    }
}

And add Review

namespace Meals.Service.Models
{
    public class Review
    {
        public int Id { get; set; }
        public string Description { get; set; }
        public int Rating { get; set; }
        public int MealId { get; set; }
    }
}

Add these entities to the IdentityDbContext in MealsContext.cs

namespace Meals.Service.Core
{
    using Microsoft.AspNet.Identity.EntityFramework;
    using Models;
    using System.Data.Entity;

    public class MealsContext : IdentityDbContext
    {
        public DbSet<Meal> Meals { get; set; }
        public DbSet<Review> Reviews { get; set; }
    }
}

Abstractions

We need to abstract a couple of classes that we need to make use of, in order to keep our code clean and ensure that it works correctly.

Under the Core folder, add the following classes

namespace Meals.Service.Core
{
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.EntityFramework;

    public class MealUserManager : UserManager<IdentityUser>
    {
        public MealUserManager() : base(new MealUserStore())
        {
        }
    }
}

We will make heavy use of the UserManager<T> in our project, and we don’t want to have to initialise it with a UserStore<T> every time we want to make use of it. Whilst adding this is not strictly necessary, it does go a long way to helping keep the code clean.

Now add another class for the UserStore

namespace Meals.Service.Core
{
    using Microsoft.AspNet.Identity.EntityFramework;

    public class MealUserStore : UserStore<IdentityUser>
    {
        public MealUserStore() : base(new MealsContext())
        {
        }
    }
}

This code is really important. If we fail to tell the UserStore which DbContext to use, it falls back to some default value.

API Controller

We need to expose some data to our client (when we write it). let’s take advantage of Entity Frameworks Seed method. The Seed method will pre-populate some books and reviews automatically for us.

Kindly refer to Configuration.cs for the code.

Meals Endpoint

Create a new controller called Meals with the following code

namespace Meals.Service.Controllers
{
    using Core;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Web.Http;

    public class MealsController : ApiController
    {
        [HttpGet]
        public async Task<IHttpActionResult> Get()
        {
            using (var context = new MealsContext())
            {
                return Ok(await context.Meals.Include(meal => meal.Reviews).ToListAsync());
            }
        }
    }
}

Reviews Endpoint

We’re also going to enable authorized users to post reviews and delete reviews. For this we will need a ReviewsController with the relevant Post and Delete methods. Add the following code;

Create a new Web API controller called ReviewsController and add the following code

namespace Meals.Service.Controllers
{
    using Core;
    using Models;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Web.Http;
    using ViewModels;

    public class ReviewsController : ApiController
    {
        [HttpPost]
        public async Task<IHttpActionResult> Post([FromBody] ReviewViewModel review)
        {
            using (var context = new MealsContext())
            {
                var meal = await context.Meals.FirstOrDefaultAsync(b => b.Id == review.MealId);
                if (meal == null)
                {
                    return NotFound();
                }

                var newReview = context.Reviews.Add(new Review
                {
                    MealId = meal.Id,
                    Description = review.Description,
                    Rating = review.Rating
                });

                await context.SaveChangesAsync();
                return Ok(new ReviewViewModel(newReview));
            }
        }

        [HttpDelete]
        [Authorize(Roles = "Administrator")]
        public async Task<IHttpActionResult> Delete(int id)
        {
            using (var context = new MealsContext())
            {
                var review = await context.Reviews.FirstOrDefaultAsync(r => r.Id == id);
                if (review == null)
                {
                    return NotFound();
                }

                context.Reviews.Remove(review);
                await context.SaveChangesAsync();
            }
            return Ok();
        }
    }
}

The [FromBody] attribute tells Web API to look for the data for the method argument in the body of the HTTP message that we received from the client, and not in the URL. The second parameter is a view model that wraps around the Review entity itself. Add a new folder to your project called ViewModels, add a new class called ReviewViewModel and add the following code

namespace Meals.Service.ViewModels
{
    using Models;

    public class ReviewViewModel
    {
        public ReviewViewModel()
        {
        }

        public ReviewViewModel(Review review)
        {
            if (review == null)
            {
                return;
            }

            MealId = review.MealId;
            Rating = review.Rating;
            Description = review.Description;
        }

        public int MealId { get; set; }
        public int Rating { get; set; }
        public string Description { get; set; }

        public Review ToReview()
        {
            return new Review
            {
                MealId = MealId,
                Description = Description,
                Rating = Rating
            };
        }
    }
}

Note: In order to keep our API RESTful, we return the newly created entity (or its view model representation) back to the client for consumption, removing the need to re-fetch the entire data set.

Next Steps: Part 2