Secure ASP.NET Core Web API using JWT Authentication
By: Aram Tchekrekjian
Tags: Aspnetcore, Authentication, Csharp, Dotnet Core, Jwt

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 3 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.

Give your project a name like SecuringWebApiUsingJwtAuthentication

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

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 the below:

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

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.



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',

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',

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 USE [CustomersDb]GO/****** Object: Table [dbo].[Customer] Script Date: 11/9/2020 1:56:38 AM ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE 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 ONGOSET QUOTED_IDENTIFIER ONGOCREATE 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]GOSET IDENTITY_INSERT [dbo].[Customer] ONGOINSERT [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)GOINSERT [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)GOSET IDENTITY_INSERT [dbo].[Customer] OFFGOSET IDENTITY_INSERT [dbo].[Order] ONGOINSERT [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)GOINSERT [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)GOSET IDENTITY_INSERT [dbo].[Order] OFFGOALTER TABLE [dbo].[Order] WITH CHECK ADD CONSTRAINT [FK_Order_Customer] FOREIGN KEY([CustomerId])REFERENCES [dbo].[Customer] ([Id])GOALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Customer]GO

Preparing the Database Models and DbContext

Create Entities Folder, and then add Customer.cs

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

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

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

Microsoft.EntityFrameworkCore.SqlServer

Once the above packages are references 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

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

{

"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.

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

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

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

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.

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;

}

}

}

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.

Generate JSON Web Token (JWT)

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

This will include our token generation function

using Microsoft.IdentityModel.Tokens;

using SecuringWebApiUsingJwtAuthentication.Entities;

using System;

using System.IdentityModel.Tokens.Jwt;

using System.Security.Claims;

using System.Security.Cryptography;

{

public class TokenHelper

{

public const string Issuer = "

public const string Audience = "



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 namespace SecuringWebApiUsingJwtAuthentication.Helperspublic class TokenHelperpublic 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

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.

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

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 should your launchsettings.json file look:

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:

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 that 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 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

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

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

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:

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

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

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

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 Only NonBlockedCustomer policy

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

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

services.AddScoped<IOrderService, OrderService>();

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

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

Now let’s try to login with correct credentials

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

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

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.

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

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

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

or a malicious user tried tampering with the JWT

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

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

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:

Secure ASP.NET Core Web API using API Key Authentication Build RESTful APIs Using ASP.NET Core and Entity Framework Core Deploy ASP.NET Core Web API on IIS

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

https://youtu.be/vEf2ZnkwZPQ