Click here to Skip to main content
15,867,686 members
Articles / Web Development / ASP.NET / ASP.NET Core

Secure ASP.NET Core Web API using JWT Authentication

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
26 Nov 2020CPOL14 min read 13.7K   15   6
JWT is a very common and easy way to protect APIs in a standard, URL safe and cross-platform methodology.
In this tutorial, you will learn how to secure ASP.NET Core Web API using JWT Authentication. I will try to simplify this topic step-by-step while coding. We will build two endpoints, one for the customers’ login and one to get customer orders. The APIs will be connected to an SQL Server Express database all running on the local machine.

What is JWT?

JWT or JSON Web Token is basically a way to format tokens, which represent an encoded structure of data that is compact, url-safe, secure and self-contained.

JWT authentication is a standard way to communicate between APIs and clients, so both parties can make sure that the data being sent/received is trusted and verified.

JWTs should be issued by a server and digitally sign it using a cryptographically secure secret, so that it will make sure that any attacker won’t be able to tamper the payload sent within the token and impersonate the legit user.

JWT structure includes three parts, separated with dots, each of which is a base64 url-encoded string and formatted in JSON:

Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwicm9sZSI6IkFjY291bnQgTWFuYWdlciIsIm5iZiI6MTYwNDAxMDE4NSwiZXhwIjoxNjA0MDExMDg1LCJpYXQiOjE2MDQwMTAxODV9.XJLeLeUIlOZQjYyQ2JT3iZ-AsXtBoQ9eI1tEtOkpyj8

  • Header: Represents the algorithm used to hash the secret (example HMACSHA256)
  • Payload: The list of data or claims to be transferred between client and API
  • Signature: The hashing of the concatenation of Header and Payload

Because the JWT tokens are base64 encoded, you can simply explore them using jwt.io or through any online base64 decoder.

And for this particular reason, you should never save confidential information about the user within JWT.

Tutorial Prerequisites

Download and install the latest update of Visual Studio 2019 (I am using Community Edition), the Dot Net Core SDK 3.x will already come bundled with VS2019.

Download and install the latest updates of SQL Server Management Studio and SQL Server Express.

Let’s Start Our Tutorial

Let’s create a new Project in Visual Studio 2019.

Image 1

Give your project a name like SecuringWebApiUsingJwtAuthentication.

Image 2

We need to choose ASP.NET Core Web API Template, then press Create.

Image 3

Visual Studio will now create the new ASP.NET Core Web API template project for you.

Let’s remove the WeatherForecastController.cs and WeatherForecast.cs files so that we can start creating our own controllers and models.

The end result project structure should look like below:

Image 4

Preparing the Database

Once you’ve installed SQL Server Express and SQL Management Studio on your machine, you should be able to access the host normally through the below pop-up dialog:

Image 5

Now from the object explorer, right click on Databases and choose new database, give your database a name like CustomersDb.

To make the process easier and faster, you just need to run the below script, it will create the tables and insert the needed data into the CustomersDb.

SQL
USE [CustomersDb]
GO
/****** Object:  Table [dbo].[Customer]    Script Date: 11/9/2020 1:56:38 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Customer](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Username] [nvarchar](255) NOT NULL,
	[Password] [nvarchar](255) NOT NULL,
	[PasswordSalt] [nvarchar](50) NOT NULL,
	[FirstName] [nvarchar](255) NOT NULL,
	[LastName] [nvarchar](255) NOT NULL,
	[Email] [nvarchar](255) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[Active] [bit] NOT NULL,
	[Blocked] [bit] NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Order]    Script Date: 11/9/2020 1:56:38 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Order](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Status] [nvarchar](50) NOT NULL,
	[Quantity] [int] NOT NULL,
	[Total] [decimal](19, 4) NOT NULL,
	[Currency] [char](3) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[CustomerId] [int] NOT NULL,
 CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
       ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Customer] ON 
GO
INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
       [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
       VALUES (1, N'coding', N'ezVOZenPoBHuLjOmnRlaI3Q3i/WcGqHDjSB5dxWtJLQ=', _
       N'MTIzNDU2Nzg5MTIzNDU2Nw==', N'Coding', N'Sonata', N'coding@codingsonata.com', _
       CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 1)
GO
INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
       [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
       VALUES (2, N'test', N'cWYaOOxmtWLC5DoXd3RZMzg/XS7Xi89emB7jtanDyAU=', _
       N'OTUxNzUzODUyNDU2OTg3NA==', N'Test', N'Testing', N'testing@codingsonata.com', _
       CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 0)
GO
SET IDENTITY_INSERT [dbo].[Customer] OFF
GO
SET IDENTITY_INSERT [dbo].[Order] ON 
GO
INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
       [CustomerId]) VALUES (1, N'Processed', 5, CAST(120.0000 AS Decimal(19, 4)), _
       N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
       [CustomerId]) VALUES (2, N'Completed', 2, CAST(750.0000 AS Decimal(19, 4)), _
       N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
GO
SET IDENTITY_INSERT [dbo].[Order] OFF
GO
ALTER TABLE [dbo].[Order]  WITH CHECK ADD  CONSTRAINT [FK_Order_Customer] _
      FOREIGN KEY([CustomerId])
REFERENCES [dbo].[Customer] ([Id])
GO
ALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Customer]
GO

Preparing the Database Models and DbContext

Create Entities Folder, and then add Customer.cs:

JavaScript
using System;
using System.Collections.Generic;

namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class Customer
    {
        public int Id { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string PasswordSalt { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime TS { get; set; }
        public bool Active { get; set; }
        public bool Blocked { get; set; }
        public ICollection<Order> Orders { get; set; }
    }
}

Then add Order.cs:

JavaScript
using System;
using System.Text.Json.Serialization;

namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class Order
    {
        public int Id { get; set; }
        public string Status { get; set; }
        public int Quantity { get; set; }
        public decimal Total { get; set; }
        public string Currency { get; set; }
        public DateTime TS { get; set; }
        public int CustomerId { get; set; }
        [JsonIgnore]
        public Customer Customer { get; set; }
    }
}

I added the JsonIgnore attribute to the Customer object in order to hide it when doing Json serialization of the Order object.

The JsonIgnore attribute comes from System.Text.Json.Serialization namespace, so make sure you include it at the top of the Order class.

Now we will create a new Class that inherits from the DbContext of EFCore that will be used to map to our database.

Create a class with name CustomersDbContext.cs:

JavaScript
using Microsoft.EntityFrameworkCore;

namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class CustomersDbContext : DbContext
    {
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Order> Orders { get; set; }
        public CustomersDbContext
               (DbContextOptions<CustomersDbContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Customer>().ToTable("Customer");
            modelBuilder.Entity<Order>().ToTable("Order");
        }
    }
}

Visual Studio will start throwing errors now, because we need to reference NuGet Packages for EntityFramework Core and EntityFramework SQL Server.

So right click on your project name and choose manage NuGet Packages, then browse and downloaded the below packages:

Microsoft.EntityFrameworkCore

Image 6

Microsoft.EntityFrameworkCore.SqlServer

Image 7

Once the above packages are referenced in your project, you should no longer see errors from VS.

Now head to Startup.cs file and add our dbcontext into the service container inside the ConfigureServices function:

JavaScript
services.AddDbContext<CustomersDbContext>(options => options.UseSqlServer
                     (Configuration.GetConnectionString("CustomersDbConnectionString")));

Let’s open appsettings.json file and include the ConnectionStrings Section with our connection string name:

JavaScript
{
  "ConnectionStrings": {
    "CustomersDbConnectionString": "Server=Home\\SQLEXPRESS;Database=CustomersDb;
     Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Now we are done with the database mapping and connection part. We will move on to prepare our business logic within services.

Creating the Services

Create a new folder with name Requests.

We will have a LoginRequest.cs class here that will represent the username and password fields that the customer will provide to login.

JavaScript
namespace SecuringWebApiUsingJwtAuthentication.Requests
{
    public class LoginRequest
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }
}

And for this, we need a special Response object, to be returned for the valid customer which will include basic user info and their access token (in JWT format) so that they can pass it within their subsequent requests to the authorized APIs through the Authorization Header as a Bearer token.

So create another folder with name Responses and inside it, create a new file with name LoginResponse.cs:

JavaScript
namespace SecuringWebApiUsingJwtAuthentication.Responses
{
    public class LoginResponse
    {
        public string Username { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Token { get; set; }
    }
}

Create a new folder with name Interfaces:

Add a new interface ICustomerService.cs, this will include the prototype for the customer login method:

JavaScript
using SecuringWebApiUsingJwtAuthentication.Requests;
using SecuringWebApiUsingJwtAuthentication.Responses;
using System.Threading.Tasks;

namespace SecuringWebApiUsingJwtAuthentication.Interfaces
{
    public interface ICustomerService
    {
        Task<LoginResponse> Login(LoginRequest loginRequest);
    }
}

Now comes the part of implementing the ICustomerService.

Create a new Folder and name it Services.

Add a new class to it, with name CustomerService.cs:

JavaScript
using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requests;
using SecuringWebApiUsingJwtAuthentication.Responses;
using System.Linq;
using System.Threading.Tasks;

namespace SecuringWebApiUsingJwtAuthentication.Services
{
    public class CustomerService : ICustomerService
    {
        private readonly CustomersDbContext customersDbContext;
        public CustomerService(CustomersDbContext customersDbContext)
        {
            this.customersDbContext = customersDbContext;
        }

        public async Task<LoginResponse> Login(LoginRequest loginRequest)
        {
            var customer = customersDbContext.Customers.SingleOrDefault
            (customer => customer.Active && customer.Username == loginRequest.Username);

            if (customer == null)
            {
                return null;
            }
            var passwordHash = HashingHelper.HashUsingPbkdf2
                               (loginRequest.Password, customer.PasswordSalt);

            if (customer.Password != passwordHash)
            {
                return null;
            }

            var token = await Task.Run( () => TokenHelper.GenerateToken(customer));

            return new LoginResponse { Username = customer.Username, 
            FirstName = customer.FirstName, LastName = customer.LastName, Token = token };
        }
    }
}

The above login function checks in the database for the active customer’s username, password, if these conditions match, then we will generate a JWT and return it in the LoginResponse for the caller, otherwise it will just return a null value in the LoginReponse.

First, let’s create a new folder with name Helpers.

Add to it a class with name HashingHelper.cs.

This will be used to check the hash for the password in the login request to match the hashed password on the database alongside the hashing salt.

Here, we are using a key-based derivation function (PBKDF2) which it applies the HMac function in combination with a hashing algorithm (SHA-256) to the password with a salt (base64 encoded random number with size 128-bit) and repeats this as many times as specified in the iterations parameter (10000 times is the default) in our example to derive a random key from the produced result.

Key Derivation functions (or Password-Hashing functions), such as PBKDF2 or Bcrypt require a longer computation time and more resources for a password to get cracked or brute-forced because of the large number of iterations applied along with the salt, this is called key stretching.

Note: You should never save a password in the database as-is (plain text), always make sure to calculate and save the hash of the password, and use a Key Derivation function based hashing algorithm with a large key size (i.e., 256-bit or more) and random large salt (64-bit or 128-bit) to make it very difficult to crack the passwords.

Also, you should make sure to apply validation rules of strong passwords (combinations of alphanumeric and special characters) whenever you are building user registration screens or pages, as well as password retention policies, this will even maximize the level of security for the stored passwords.

JavaScript
using System;
using System.Security.Cryptography;
using System.Text;

namespace SecuringWebApiUsingJwtAuthentication.Helpers
{
    public class HashingHelper
    {
        public static string HashUsingPbkdf2(string password, string salt)
        {
            using var bytes = new Rfc2898DeriveBytes
            (password, Convert.FromBase64String(salt), 10000, HashAlgorithmName.SHA256);
            var derivedRandomKey = bytes.GetBytes(32);
            var hash = Convert.ToBase64String(derivedRandomKey);
            return hash;
        }
    }
}

Generate JSON Web Token (JWT)

Add another class to the Helpers folder with name TokenHelper.cs.

This will include our token generation function:

JavaScript
using Microsoft.IdentityModel.Tokens;
using SecuringWebApiUsingJwtAuthentication.Entities;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;

namespace SecuringWebApiUsingJwtAuthentication.Helpers
{
    public class TokenHelper
    {
        public const string Issuer = "http://codingsonata.com";
        public const string Audience = "http://codingsonata.com";
      
        public const string Secret = 
        "OFRC1j9aaR2BvADxNWlG2pmuD392UfQBZZLM1fuzDEzDlEpSsn+
         btrpJKd3FfY855OMA9oK4Mc8y48eYUrVUSw==";
      
        //Important note***************
        //The secret is a base64-encoded string, always make sure to 
        //use a secure long string so no one can guess it. ever!.a very recommended approach 
        //to use is through the HMACSHA256() class, to generate such a secure secret, 
        //you can refer to the below function 
        //you can run a small test by calling the GenerateSecureSecret() function 
        //to generate a random secure secret once, grab it, and use it as the secret above 
        //or you can save it into appsettings.json file and then load it from them, 
        //the choice is yours

        public static string GenerateSecureSecret()
        {
            var hmac = new HMACSHA256();
            return Convert.ToBase64String(hmac.Key);
        }

        public static string GenerateToken(Customer customer)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key =  Convert.FromBase64String(Secret);

            var claimsIdentity = new ClaimsIdentity(new[] { 
                new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString()),
                new Claim("IsBlocked", customer.Blocked.ToString())
            });
            var signingCredentials = new SigningCredentials
            (new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = claimsIdentity,
                Issuer = Issuer,
                Audience = Audience,
                Expires = DateTime.Now.AddMinutes(15),
                SigningCredentials = signingCredentials,
                
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }
    }
}

We need to reference another library here which is the Microsoft.AspNetCore.Authentication.JwtBearer:

Image 8

Let’s take a closer look at the GenerateToken function:

We are passing the customer object, we can use as many properties as we want and add them to the claims that will be embedded within the Token. But for this tutorial, we will be just embedding the id property of the Customer.

The JWT relies on digitally signing algorithms, one of these algorithms that is recommended and we are using here is the HMac Hashing algorithm using a 256-bit key size.

We are generating the key from a random secret that we previously generated using the HMACSHA256 Class. You can use any random string, but make sure to use a long and hard to guess text, and better go with the HMACSHA256 class as illustrated in the previous code sample.

You can save the generated secret in a constant or appsettings and load it within the Startup.cs file.

Creating the Controllers

Now we need to consume the Login method of the CustomerService from the CustomersController.

Create a new folder and name it Controllers.

Add a new file CustomersController.cs, it will have one POST method that will accept the username and the password and return the JWT token along with other customer details if the login process was successful, otherwise it will return 400 bad request.

JavaScript
using Microsoft.AspNetCore.Mvc;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requests;
using System.Threading.Tasks;

namespace SecuringWebApiUsingJwtAuthentication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly ICustomerService customerService;

        public CustomersController(ICustomerService customerService)
        {
            this.customerService = customerService;
        }
        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login(LoginRequest loginRequest)
        {
            if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || 
                string.IsNullOrEmpty(loginRequest.Password))
            {
                return BadRequest("Missing login details");
            }

            var loginResponse = await customerService.Login(loginRequest);

            if (loginResponse == null)
            {
                return BadRequest($"Invalid credentials");
            }

            return Ok(loginResponse);
        }
    }
}

As you can see here, we have define one POST method /Login that accepts the LoginRequest (username and password), it does a basic validation on the input, and it calls the Login method of the customer service.

We will be injecting the CustomerService through the Controller’s constructor using the public Interface ICustomerService, we will need to define this injection in the Startup’s ConfigureServices function:

JavaScript
services.AddScoped<ICustomerService, CustomerService>();

Now, just before running the API, we can configure the launching URL and also we can know the port number for both http and https within the IIS Express object under IIS Settings.

This is how your launchsettings.json file should look:

JavaScript
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:60057",
      "sslPort": 44375
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "SecuringWebApiUsingJwtAuthentication": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Now if you run the API on your local machine, you should be able to call the login method and generate your first JSON Web Token.

Testing Login on Postman

Keep the browser open, and open postman.

In a new request tab, put down the localhost and port number provided to you in the settings once you run the application.

From the body, choose raw and JSON and put the below JSON object that you will be using to login to the customers database through our RESTful API.

Here is the request/response from postman:

Image 9

Here, we have our first JWT.

Let’s prepare our API to receive this token, validate it, find a claim in it and then return a response for the caller.

There are multiple ways in which you can use to validate your APIs and authorize your users:

  • Policy-based authorization, which can also include defining Roles and Requirements, as per the dot net core team, this is the recommended approach to implement the API Authentication through a fine-grained approach. You can read more about Policy-based authorization on this Stackoverflow thread.
  • Having a custom middleware that validates the JWT passed in the request headers on the APIs that have the Authorize attribute decorated on them.
  • Having a custom attribute set on one or more Controller methods that validates the Request Headers Collection for the JWT Authorization header.

In this tutorial, I will be using the Policy-based authentication in its simplest form, just to show you that you can apply the policy-based approach in securing your ASP.NET Core Web APIs.

The Difference between Authentication and Authorization

Authentication is the process of verifying that a user has the rights to access your APIs. Usually, an unauthenticated user trying to access your APIs will receive an http 401 Unauthorized Response.

Authorization is the process of verifying if the authenticated user has the proper rights or permissions to access a specific API. Usually, an unauthorized user trying to access your API that is only valid for a specific role or requirement will receive an http 403 Forbidden Response.

Configuring Authentication and Authorization

Now let’s add the authentication and authorization configurations in our startup file.

Inside the ConfigureServices method, we need to define the authentication scheme and its properties and then we will define the authorization options.

In the authentication part, we will be using the Default Scheme of the JwtBearer, and we will define the TokenValidationParamters, so that we validate the IssuerSigningKey to make sure that the JWT in question is signed using the correct Security Key with the same Secret.

In the authorization part, we will be adding a policy that, when specified on an endpoint with the Authorize attribute, it will authorize only the customers whose accounts are not blocked.

A blocked customer will still be able to access other endpoints that don’t have the policy defined on them, but for the endpoints who define the OnlyNonBlockedCustomer policy, blocked customers will be denied access with a 403 Forbidden response.

This policy will be validated through a custom requirement that implements the IAuthorizationRequirement and a handler that inherits the AuthorizationHandler<T> class where T is the customer requirement class.

First, create a folder and name it Requirements.

Add a new class with name CustomerStatusRequirement.cs.

JavaScript
using Microsoft.AspNetCore.Authorization;

namespace SecuringWebApiUsingJwtAuthentication.Requirements
{
    public class CustomerBlockedStatusRequirement : IAuthorizationRequirement
    {
        public bool IsBlocked { get; }
        public CustomerBlockedStatusRequirement(bool isBlocked)
        {
            IsBlocked = isBlocked;
        }
    }
}

Then create another folder and name it Handlers.

Add a new class with name CustomerBlockedStatusHandler.cs:

JavaScript
using Microsoft.AspNetCore.Authorization;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Requirements;
using System;
using System.Threading.Tasks;

namespace SecuringWebApiUsingJwtAuthentication.Handlers
{
    public class CustomerBlockedStatusHandler : 
           AuthorizationHandler<CustomerBlockedStatusRequirement>
    {
        protected override Task HandleRequirementAsync
        (AuthorizationHandlerContext context, CustomerBlockedStatusRequirement requirement)
        {
            var claim = context.User.FindFirst(c => c.Type == "IsBlocked" && 
                                               c.Issuer == TokenHelper.Issuer);
            if (!context.User.HasClaim(c => c.Type == "IsBlocked" && 
                                            c.Issuer == TokenHelper.Issuer))
            {
                return Task.CompletedTask;
            }

            string value = context.User.FindFirst(c => c.Type == "IsBlocked" && 
                                                  c.Issuer == TokenHelper.Issuer).Value;
            var customerBlockedStatus = Convert.ToBoolean(value);

            if (customerBlockedStatus == requirement.IsBlocked)
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Finally, let’s add all the authentication and authorization configurations to the services collection:

JavaScript
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateIssuerSigningKey = true,
                            ValidIssuer = TokenHelper.Issuer,
                            ValidAudience = TokenHelper.Audience,
                            IssuerSigningKey = new SymmetricSecurityKey
                                (Convert.FromBase64String(TokenHelper.Secret))
                        };                        
                    });

services.AddAuthorization(options =>
            {
                options.AddPolicy("OnlyNonBlockedCustomer", policy => {
                    policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));
                
                });
            });

services.AddSingleton<IAuthorizationHandler, CustomerBlockedStatusHandler>();

For these, we need to include the following namespaces:

JavaScript
using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Handlers;
using SecuringWebApiUsingJwtAuthentication.Requirements;

Now the above won’t work by itself, the authentication and authorization have to be included within the ASP.NET Core API pipeline in the other startup method which is the Configure method:

JavaScript
app.UseAuthentication();
app.UseAuthorization();

So here, we are done with configuring our ASP.NET Core Web API using JWT Authentication.

You can check the official docs for more information about Policy-Based Authorization in ASP.NET Core.

Creating the OrderService

We will need a new service that will specialize in the orders.

Create a new Interface under Interfaces Folder with name IOrderService.cs:

JavaScript
using SecuringWebApiUsingJwtAuthentication.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SecuringWebApiUsingJwtAuthentication.Interfaces
{
    public interface IOrderService
    {
        Task<List<Order>> GetOrdersByCustomerId(int id);
    }
}

The interface includes one method that will retrieve the orders for a given customer by Customer Id.

Let’s implement this interface.

Create a new class under Services Folder with name OrderService.cs:

JavaScript
using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace SecuringWebApiUsingJwtAuthentication.Services
{
    public class OrderService : IOrderService
    {
        private readonly CustomersDbContext customersDbContext;

        public OrderService(CustomersDbContext customersDbContext)
        {
            this.customersDbContext = customersDbContext;
        }
        public async Task<List<Order>> GetOrdersByCustomerId(int id)
        {
            var orders = await customersDbContext.Orders.Where
                         (order => order.CustomerId == id).ToListAsync();
        
            return orders;
        }
    }
}

Creating the OrdersController

Now we need to create a new endpoint that will use the Authorize attribute with the OnlyNonBlockedCustomer policy.

Add a new Controller under your Controllers Folder, name it OrdersController.cs:

JavaScript
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SecuringWebApiUsingJwtAuthentication.Interfaces;

namespace SecuringWebApiUsingJwtAuthentication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly IOrderService orderService;

        public OrdersController(IOrderService orderService)
        {
            this.orderService = orderService;
        }

        [HttpGet()]
        [Authorize(Policy = "OnlyNonBlockedCustomer")]
        public async Task<IActionResult> Get()
        {
            var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;

            var claim = claimsIdentity.FindFirst(ClaimTypes.NameIdentifier);
            
            if (claim == null)
            {
                return Unauthorized("Invalid customer");
            }

            var orders = await orderService.GetOrdersByCustomerId(int.Parse(claim.Value));

            if (orders == null || !orders.Any())
            {
                return BadRequest($"No order was found");
            }

            return Ok(orders);
        }
    }
}

We will create a GET Method that will be used to retrieve the Customer’s Orders. This method will be decorated with the Authorize Attribute and defined an access policy for only the Non Blocked Customers.

That’s our requirement for this policy.

Any blocked customer who will try to get their orders, even if the customer is authenticated properly, will receive a 403 Forbidden request, because this customer is not authorized to access this specific endpoint.

We need to include the OrderService within the Startup.cs file.

Add the below just under the CustomerService line.

JavaScript
services.AddScoped<IOrderService, OrderService>();

This is the full view of the Startup.cs file to double-check with yours.

JavaScript
using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Handlers;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requirements;
using SecuringWebApiUsingJwtAuthentication.Services;

namespace SecuringWebApiUsingJwtAuthentication
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. 
        // Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {           
            services.AddDbContext<CustomersDbContext>
               (options => options.UseSqlServer(Configuration.GetConnectionString
               ("CustomersDbConnectionString")));

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateIssuerSigningKey = true,
                            ValidIssuer = TokenHelper.Issuer,
                            ValidAudience = TokenHelper.Audience,
                            IssuerSigningKey = new SymmetricSecurityKey
                            (Convert.FromBase64String(TokenHelper.Secret))
                        };
                        
                    });

            services.AddAuthorization(options =>
            {
                options.AddPolicy("OnlyNonBlockedCustomer", policy => {
                    policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));
                });
            });

            services.AddSingleton<IAuthorizationHandler, CustomerBlockedStatusHandler>();

            services.AddScoped<ICustomerService, CustomerService>();
            services.AddScoped<IOrderService, OrderService>();
            services.AddControllers();
        }

        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Testing on Postman

Run the application and open Postman.

Let’s try to login with a wrong password:

Image 10

Now let’s try to login with correct credentials:

Image 11

If you take the above token and validate it on jwt.io, you will see the header and payload details in it:

Image 12

Now let’s test the get orders endpoint, we will take the token string and pass it in the Authorization Header as Bearer Token:

Image 13

So Why Did Our API Return a 403 Forbidden?

If you go back one step before, you will notice that in the claims our customer is blocked (“IsBlocked”: True), which tests our policy and requirement for this endpoint that only non blocked customers are authorized to access this endpoint.

To make this work, we will either unblock this customer or try to login with another customer. So let’s go unblock this customer.

Head back to your database, and change the user’s Blocked value to False.

Now open Postman again and login with the same user, so that we get a new JWT that includes the updated claim value for IsBlocked claim type.

Image 14

Now, let’s peak at our new JWT in jwt.io:

Image 15

Do you notice the difference now? This customer is no longer blocked now, because we obtained a new JWT that includes the refreshed claim that is reading from the database.

So let’s try to access our secure endpoint using this new JWT.

Image 16

It works now! Our customer has successfully passed the policy’s requirement for being a non blocked customer and thus the orders are now showing.

Let’s see what will happen if a user tries to access this endpoint without passing the Authorization header:

Image 17

or a malicious user tried tampering with the JWT:

Image 18

JWT is tamper-proof, so no one can fool around with it.

I hope this tutorial provided you with a good understanding of API Security and JWT Authentication. You should now be able to Secure ASP.NET Core Web API using JWT Authentication.

Please let me know if you have any question or comments.

You can find the full source code on Github.

Check my other articles related to ASP.NET Core:

Summary

API Security is a rather important and crucial topic that should be handled properly to guarantee proper authentication and authorization of the users to properly access your backend resources and protect your data from unauthorized access.

JWT is a very common and easy way to protect APIs in a standard, url-safe and cross-platform methodology. Claims are safely passed through the JWT between communicating parties.

Bonus

For this great topic, I have chosen a great piece of music for my great readers.

Enjoy the brilliant tunes of Henry Purcell: Ground in C Minor by Hanneke van Proosdij, harpsichord

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Jordan Jordan
A passionate software developer with 13+ years of overall experience in various development languages including C#/vb.net, java. The technologies I mostly focus on are: ASP.NET Core, Android, Angular

I currently work as a Corporate Technical Manager in the Digital Solutions Team at Aramex International in Amman, Jordan.

Comments and Discussions

 
GeneralMy vote of 5 Pin
buituan111-Jun-21 5:44
buituan111-Jun-21 5:44 
QuestionPassword salt Pin
wilcodk23-Jan-21 5:11
wilcodk23-Jan-21 5:11 
Questionerror Pin
Omar Carrera9-Dec-20 6:13
Omar Carrera9-Dec-20 6:13 
QuestionMy vote of 5 Pin
Igor Ladnik8-Dec-20 15:03
professionalIgor Ladnik8-Dec-20 15:03 
GeneralMy vote of 5 Pin
Vangel Kolarov30-Nov-20 21:50
Vangel Kolarov30-Nov-20 21:50 
QuestionSQL Server vs (eg) MSAD Pin
Garth J Lancaster26-Nov-20 19:47
professionalGarth J Lancaster26-Nov-20 19:47 
The only line I can see where the SQL Server is referenced appears to be in the services setup - ie
Quote:
services.AddDbContext<customersdbcontext>(options => options.UseSqlServer(Configuration.GetConnectionString("CustomersDbConnectionString")));


How would one use MSAD instead ? I'm having trouble finding a reference to 'services vs Authentication options'

cheers, great article

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.