Published on

How To Add JWT Authentication To An ASP.NET Core API

Authors
  • avatar
    Name
    Jacob Toftgaard Rasmussen
    Twitter

In this 2 part article series I will show you how you can create an online platform using React that will communicate with a .Net 7 backend. It will include authentication using .NET Identity, and the user information will be saved into a Postgres database.

The goal

After completing this guide you will have created a .NET 7 web API that exposes a secure endpoint. The secure endpoint can only be accessed by users who have a registered account in your system. To allow users to register an account in your system we will also create a small React app that makes it possible to sign up, login, and finally access the secure endpoint. To store the user data we will be spinning up a PostgreSQL docker container.
Naturally there are many details and technologies in play in this guide and because I want to prevent the articles from becoming too long I will have to leave out explanations for some of the steps that we do. All in all this is the guide for you who wants to build a web API with authentication for the first time.

Prerequisites

To best be able to follow along and code along I suggest that you have experience with, (or at least are not frightened), by the following technologies:

Experience:

  • C#
  • Typescript and React
  • Docker (don't worry, this is not a big deal in this guide)
  • PostgreSQL (also not a big deal)

Tools:

  • Visual studio or Rider
  • Docker
  • Postman
  • VSC (or your preferred frontend editor)
  • A browser

Alright enough talk! Let's get going!

Setting up a new .NET API solution

Let's begin by creating the backend API using .NET 7. For this I will scaffold a .NET solution and project using Rider, (an IDE from JetBrains), but you can also use Visual studio which is free.
I choose an ASP.NET Core Web Application as the template. I give the solution a name, in this case: ApiWithAuth. If you want, you can tick the box for Put solution and project in the same directory and optionally also the Create Git repository. Choose 7.0 for the SDK, and Web API as the Type. I choose no authentication for Auth, because I want to create it myself. Finally you can choose to enable docker support if you wish, but this is not required for this guide. Finally, click Create.

Screenshot of the new solution options

API First steps

When you create a .NET project like we just did a lot of code is provided to you. Your file explorer should now look similar like this structure:

-- /ApiWithAuth (solution)
  -- /ApiWithAuth (project)
    -- /Controllers
      -- WeatherForecastController.cs
    -- Appsettings.json
    -- Appsettings.Development.json
    -- Program.cs
    -- WeatherForecast.cs
Screenshot of the swagger UI for the API

Next, let's import the necessary nuget packages that we need for this project.

Open the csproj file for the project, (How to edit the csproj file), it should currently look like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0"/>
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
    </ItemGroup>

</Project>

Inside the <ItemGroup> tag add the following package references (just below the Swashbuckle.AspNetCore package reference):

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.11" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

We are going to need all of these for the final version of our API.


Securing the WeatherForecast endpoint

To secure the endpoint behind an authentication scheme we need to edit two files, Program.cs and the WeatherForecastController.cs
Let's start out with the latter.
Find the line where "GetWeatherForecast" endpoint is defined and then add , Authorize inside the square brackets like below:

// ...
[HttpGet(Name = "GetWeatherForecast"), Authorize]
    public IEnumerable<WeatherForecast> Get()
    { // ...

This will cause our API to check if a request is authorized to access this endpoint. In our case this will check if the user is signed in or not.

If we attempt to access the endpoint through the Swagger UI like we did before it will not work quite yet. As it is now, an internal server error will occur because an exception is thrown. The reason for this is that we have not yet set up an Authentication scheme in Program.cs. So let's go ahead and do that now!

Inside the file find the line where we add builder.Services.AddSwaggerGen()and below it add the following builder service:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ClockSkew = TimeSpan.Zero,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "apiWithAuthBackend",
            ValidAudience = "apiWithAuthBackend",
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("!SomethingSecret!")
            ),
        };
    });

This will add a JWT token authentication scheme to our API. In the options.TokenValidationParameters we specify the options for the authentication. Here, the strings, "apiWithAuthBackend" and "!SomethingSecret!" are important and we will need to use those exact same strings later on in the guide.

Note
It would definitely be best practice to move the ValidIssuer and ValidAudience strings into a configuration file like the appsettings.json file, and the IssuerSigningKey should be kept secret in a place like an .env file or in user secrets.
I have not done it here in order to keep the guide simpler and easier to follow.

Next, we also need to add the following line

app.UseAuthentication();

Add it just above the line app.UseAuthorization(), which you will find at the bottom of the Program.cs file.

At this point the Program.cs file should look like this:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ClockSkew = TimeSpan.Zero,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "apiWithAuthBackend",
            ValidAudience = "apiWithAuthBackend",
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("!SomethingSecret!")
            ),
        };
    });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

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

app.MapControllers();

app.Run();
Screenshot of the swagger UI showing the unauthorized error

So far so good. We now have a secured endpoint that requires a JWT token in order to be accessed. The next step is to set up ASP.NET Identity which we will use to handle our users, along with a PostgreSQL database such that we can persist our users as well.

Running a PostgreSQL docker container

For this part of the guide you will need to be able to run a docker container, or otherwise have a PostgreSQL database that you can connect to.
Once you have docker available on your computer open a terminal and execute this command: docker run --name auth-api-db -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres

This will download the postgres docker image and start a docker container with a postgres database on it. You will then be able to connect to the database on your localhost using port 5432. If you want to stop the container you can use the command docker stop auth-api-db, and similarly to start it again you use docker start auth-api-db

Now that we have a database up and running let's go back to our .NET project. We need to create an IdentityUserContext such that we can connect to the database and manage our uses using the API. We create a new file called UsersContext.cs and for simplicity's sake we will store it at the root of our project.

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace ApiWithAuth;

public class UsersContext : IdentityUserContext<IdentityUser>
{
    public UsersContext (DbContextOptions<UsersContext> options)
        : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // It would be a good idea to move the connection string to user secrets
        options.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=mysecretpassword");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

We provide a connection string to options.UseNpgsql() and this is will allow us to connect to the database running in the docker container. If you have chosen to use a different database than the docker image that I suggested then you need to modify the connection string to match your database setup.

And we also need to add our new UsersContext as a DbContext in our Program.cs file.

// Add services to the container.
//...
builder.Services.AddDbContext<UsersContext>();
//...

Connect and Populate the database with tables

Screenshot of the swagger UI showing the unauthorized error

When that is working we need to create tables in which we will store our users' information. With our current setup we can use Entity Framework to make those tables for us.
Open up a new terminal at the root of the project directory. If you already have the Entity Framework Core CLI tools installed then you can skip the following command, otherwise you can install the tool using: dotnet tool install --global dotnet-ef
Next, use the command dotnet ef migrations add initialMigration
Followed by dotnet ef database update
This will create a new migration which specifies all the new tables that are going to be created for us, and then the database is updated using that migration.

Once the command has finished running you can refresh your connection to the database and you should now be able to find 5 new tables in it.

\_\_EFMigrationsHistory
AspNetUserClaims
AspNetUserLogins
AspNetUsers
AspNetUserTokens

Once we start to register some new users, then their information will be saved in the AspNetUsers table.

Register new users

In order to create the functionality to register new users we will create two new file. RegistrationRequest.cs and AuthController.cs. The first file will include a class which will represent the information that a user fills out when signing up a new account. The auth controller will expose the registration endpoint. RegistrationRequest.cs will look like this.

using System.ComponentModel.DataAnnotations;

namespace ApiWithAuth.Controllers;

    public class RegistrationRequest
    {
        [Required]
        public string Email { get; set; } = null!;

        [Required]
        public string Username { get; set; } = null!;

        [Required]
        public string Password { get; set; } = null!;
    }

And AuthController.cs will look like this.

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace ApiWithAuth.Controllers;

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;

    public AuthController(UserManager<IdentityUser> userManager)
    {
        _userManager = userManager;
    }

    [HttpPost]
    [Route("register")]
    public async Task<IActionResult> Register(RegistrationRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var result = await _userManager.CreateAsync(
            new IdentityUser { UserName = request.Username, Email = request.Email},
            request.Password
        );

        if (result.Succeeded)
        {
            request.password = "";
            return CreatedAtAction(nameof(Register), new {email = request.Email}, request);
        }

        foreach (var error in result.Errors) {
            ModelState.AddModelError(error.Code, error.Description);
        }
        return BadRequest(ModelState);
    }
}

The register endpoint requires the user to post a request that includes a RegistrationRequest, and it will then use the _userManager to create that new user and store it in the database.
But wait...
How does the _userManager have a connection the the database?
At the moment it does not, because we are missing another service in the program.cs file. So, open that file and add the following service.

//...
builder.Services
    .AddIdentityCore<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.User.RequireUniqueEmail = true;
        options.Password.RequireDigit = false;
        options.Password.RequiredLength = 6;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequireLowercase = false;
    })
    .AddEntityFrameworkStores<UsersContext>();
//...

Adding this specifies the requirements that we have for new user registrations. For example that the user must have a unique email address and that the password must consist of at least 6 characters. Finally, the last line .AddEntityFrameworkStores<UsersContext>(); specifies the context, (the connection to the database), where our users' information will be handled and stored.

Screenshot of the swagger UI showing the new registration endpoint

Authentication / logging in

Now that we have a user the next step is to use the email and password to login and receive an access token. However, before we can receive the token we need to add the code that can generate an access token. For that purpose we create a new class in a file called TokenService.cs. To prevent this guide from becoming too long I will not explain the details of the methods inside it, however I encourage you to research on your own and learn about JWT token generation. The class looks as follows:

using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;

namespace ApiWithAuth
{
    public class TokenService
    {
        private const int ExpirationMinutes = 30;
        public string CreateToken(IdentityUser user)
        {
            var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes);

            var token = CreateJwtToken(
                CreateClaims(user),
                CreateSigningCredentials(),
                expiration
            );

            var tokenHandler = new JwtSecurityTokenHandler();

            return tokenHandler.WriteToken(token);
        }

        private JwtSecurityToken CreateJwtToken(List<Claim> claims, SigningCredentials credentials,
            DateTime expiration) =>
            new(
                "apiWithAuthBackend",
                "apiWithAuthBackend",
                claims,
                expires: expiration,
                signingCredentials: credentials
            );

        private List<Claim> CreateClaims(IdentityUser user)
        {
            try
            {
                var claims = new List<Claim>
                {
                    new Claim(JwtRegisteredClaimNames.Sub, "TokenForTheApiWithAuth"),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
                    new Claim(ClaimTypes.NameIdentifier, user.Id),
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(ClaimTypes.Email, user.Email)
                };
                return claims;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        private SigningCredentials CreateSigningCredentials()
        {
            return new SigningCredentials(
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes("!SomethingSecret!")
                ),
                SecurityAlgorithms.HmacSha256
            );
        }
    }
}

The public method CreateToken is the function that we will call from outside of this class, and it will then return a brand new JWT token for our user. Make sure that the signing secret used in the method CreateSigningCredentials() is the same as the one you specified in the program.cs file, the same applies to the two strings passed to the JwtSecurityToken constructor inside the method called CreateJwtToken. Now we also need to add this class as a service in program.cs.

//... other services like the AddDbContext
builder.Services.AddScoped<TokenService, TokenService>();
//...

Next let's create two new classes. The first will represent the authentication request, which is the information that a user must send in order to login. The second is an authentication response, which is the response that the user receives after successfully logging in. Both classes are quite simple and look like this (If you prefer you can also put them inside the same file):

AuthRequest.cs

namespace ApiWithAuth;

    public class AuthRequest
    {
        public string Email { get; set; } = null!;
        public string Password { get; set; } = null!;
    }

AuthResponse.cs

namespace ApiWithAuth;

    public class AuthResponse
    {
        public string Username { get; set; } = null!;
        public string Email { get; set; } = null!;
        public string Token { get; set; } = null!;
    }

Finally, in order to login we only need the endpoint to actually do so. We can place that endpoint inside AuthController.cs, so let's open that file and add the following code snippet below the register endpoint.

    [HttpPost]
    [Route("login")]
    public async Task<ActionResult<AuthResponse>> Authenticate([FromBody] AuthRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var managedUser = await _userManager.FindByEmailAsync(request.Email);

        if (managedUser == null)
        {
            return BadRequest("Bad credentials");
        }

        var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password);

        if (!isPasswordValid)
        {
            return BadRequest("Bad credentials");
        }

        var managedUser = _context.Users.FirstOrDefault(u => u.Email == request.Email);
        if (managedUser is null)
            return Unauthorized();

        var accessToken = _tokenService.CreateToken(managedUser);

        return Ok(new AuthResponse
        {
            Username = managedUser.UserName,
            Email = managedUser.Email,
            Token = accessToken,
        });
    }

Your IDE should now be complaining that _context and _tokenService do not exist. To fix this we just need to add them at the start of the file as private properties of the class, and instantiate them in the constructor. We modify the constructor and the properties to look like this:

//...
    private readonly UserManager<IdentityUser> _userManager;
    private readonly UsersContext _context;
    private readonly TokenService _tokenService;

    public AuthController(UserManager<IdentityUser> userManager, UsersContext context, TokenService tokenService)
    {
        _userManager = userManager;
        _context = context;
        _tokenService = tokenService;
    }
//...
Screenshot of the swagger UI showing the new registration endpoint

Using the token to access the weather forecast

We are almost at the end of setting up the backend API, there is only one thing missing before we can use the token to access our weather forecast endpoint. Currently, the swagger UI does now allow us to attach an access token to our requests. To fix that we head into the program.cs file and exchange the line

builder.Services.AddSwaggerGen();

with this code snippet

builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});
Screenshot of the swagger UI showing the Authorize button

Congratulations on making it this far! Really good job.
You have now created a secure API endpoint only accessible by authenticated users. As well as the functionality for users to sign up and to login.

For the rest of this guide we will head over to the frontend side of things, where we will create a small React app that can make requests to this endpoint.