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

Apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6

Rate me:
Please Sign up or sign in to vote.
4.95/5 (8 votes)
17 Feb 2022CPOL11 min read 18.9K   17   2
How to apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6
In this tutorial, we will build a simple, secure and reliable RESTful API project to properly authenticate users and authorize them to perform operations on the APIs.

In this tutorial, we will learn how to Apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6. We will build a simple, secure and reliable RESTful API project to properly authenticate users and authorize them to perform operations on the APIs.

We will use the latest and greatest version of Visual Studio 2022 – Community Edition to build our ASP.NET Core Web API using the fastest .NET Standard framework which is .NET 6 and C# 10.

The good news is that VS 2022 comes bundled with the latest version of .NET and C#, so you don’t have to worry about searching and installing any of them. So, if you haven’t got VS 2022 already, make sure you download and install Visual Studio 2022 so we can get started with our tutorial to Apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6.

In a previous tutorial, we learned how to secure ASP.NET Core Web API using JWT Authentication but that was only using access tokens. Now this is a new tutorial built from the ground up explaining everything about JWT Authentication while using both access and refresh tokens.

Access Tokens vs Refresh Tokens

We use an access token to grant a user the proper authorization to access some resources on the server when it is provided in the Authorization header. An access token is usually short-timed and signed, as for a JWT Token, this will include the signature, claims, headers.

On the other hand, a refresh token is usually a reference that can be used only to refresh the access token. Such token is usually persisted in a backend storage and can be used to revoke access for users who, for example, are no longer eligible to access these resources or in the case of a malicious user who stole an access token.

In such cases, you can just remove the refresh token for these devices, so once their access token has expired, they won’t be able to renew (refresh) it using the revoked refresh token because their once-valid refresh token is no longer valid and they will no longer be able to access your resources. Therefore, the user will be signed out in the app or web so they will have to re login and go through the usual login process again.

Now enough with the rigid texts, let’s jump right into building our APIs that will implement the JWT Authentication using both access and refresh tokens using ASP.NET Core Web API in NET 6.

Starting the Tutorial

We will build a simple tasks management System, that allows the authenticated user to manage their own tasks. This is just a simple and basic representation of the tasks management system.

Feel free to fork it from Github and build on it for your personal projects.

Database Preparation

For most of my tutorials, I am using SQL Server Express to create the database and the tables needed. So make sure you download and install the latest version of SQL Server Management Studio and SQL Server Express.

Once both are installed, open SQL Server Management Studio and connect to your local machine where the SQL Server Express is installed:

Image 1

From the object explorer, right-click on databases and choose “Create new database” give it a name like TasksDb:

Image 2

Then run the below commands to create the table and populate it with the data required for this tutorial:

SQL
USE [TasksDb]
GO
/****** Object:  Table [dbo].[RefreshToken]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[RefreshToken](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NOT NULL,
	[TokenHash] [nvarchar](1000) NOT NULL,
	[TokenSalt] [nvarchar](50) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[ExpiryDate] [smalldatetime] NOT NULL,
 CONSTRAINT [PK_RefreshToken] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Task]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Task](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[IsCompleted] [bit] NOT NULL,
	[TS] [smalldatetime] NOT NULL,
 CONSTRAINT [PK_Task] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[User]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[User](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Email] [nvarchar](50) NOT NULL,
	[Password] [nvarchar](255) NOT NULL,
	[PasswordSalt] [nvarchar](255) NOT NULL,
	[FirstName] [nvarchar](255) NOT NULL,
	[LastName] [nvarchar](255) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[Active] [bit] NOT NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Task] ON 
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (1, 1, N'Blog about Access Token and Refresh Token Authentication', _
1, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (3, 1, N'Vaccum the House', 0, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (4, 1, N'Farmers Market Shopping', 0, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (5, 1, N'Practice Juggling', 0, CAST(N'2022-01-15T00:00:00' AS SmallDateTime))
GO
SET IDENTITY_INSERT [dbo].[Task] OFF
GO
SET IDENTITY_INSERT [dbo].[User] ON 
GO
INSERT [dbo].[User] ([Id], [Email], [Password], [PasswordSalt], [FirstName], [LastName], _
[TS], [Active]) VALUES (1, N'coding@codingsonata.com', _
N'miLgvYoSVrotOON6/lRp8ACrrbAxCPCmsrsy355x/DI=', _
N'L5hziA8V93SNGTlYdz+meS0B6DPzB3IwsRhDf1vO1GM=', N'Coding', N'Sonata', _
CAST(N'2022-01-14T00:00:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[User] ([Id], [Email], [Password], [PasswordSalt], [FirstName], [LastName], _
[TS], [Active]) VALUES (2, N'test@codingsonata.com', _
N'Fm7/SI9lYAFglzWXLD5oLz0cuq00MQmPkzDZ+nDZNmc=', _
N'kjgIDmRKgUbbWypCOOUHuxlQzZAszdEKw358ds4Xyc4=', N'test', N'postman', _
CAST(N'2022-01-16T14:23:00' AS SmallDateTime), 1)
GO
SET IDENTITY_INSERT [dbo].[User] OFF
GO
ALTER TABLE [dbo].[RefreshToken]  WITH CHECK ADD  CONSTRAINT [FK_RefreshToken_User] _
FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([Id])
GO
ALTER TABLE [dbo].[RefreshToken] CHECK CONSTRAINT [FK_RefreshToken_User]
GO
ALTER TABLE [dbo].[Task]  WITH CHECK ADD  CONSTRAINT [FK_Task_User] FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([Id])
GO
ALTER TABLE [dbo].[Task] CHECK CONSTRAINT [FK_Task_User]
GO

Project Creation

Open Visual Studio 2022, and create a new project of type ASP.NET Core Web API:

Image 3

Give it a name like TasksApi:

Image 4

Then choose .NET 6.0 and create the project:

Image 5

Once VS completes the initialization of the project, press F5 to do an initial run for the template project to make sure that it works fine.

Now, let’s remove some unneeded classes from the template project. From Solution Explorer, delete WeatherForecastController and WeatherForecast files.

Image 6

Entity Framework Core and DbContext

Let’s add Nuget package for EF Core and EF Core SQL:

Image 7

Entities

Now let’s create the needed entities that will bind to the database tables through the EF Core DbContext class.

We will create three entities that will map to the Tasks database.

RefreshToken

C#
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable

namespace TasksApi
{
    public partial class RefreshToken
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string TokenHash { get; set; }
        public string TokenSalt { get; set; }
        public DateTime Ts { get; set; }
        public DateTime ExpiryDate { get; set; }
        public virtual User User { get; set; }
    }
}

Task

C#
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using System;
using System.Collections.Generic;

namespace TasksApi
{
    public partial class Task
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }

        public virtual User User { get; set; }
    }
}

User

C#
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using System;
using System.Collections.Generic;

namespace TasksApi
{
    public partial class User
    {
        public User()
        {
            RefreshTokens = new HashSet<RefreshToken>();
            Tasks = new HashSet<Task>();
        }

        public int Id { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
        public string PasswordSalt { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime Ts { get; set; }
        public bool Active { get; set; }

        public virtual ICollection<RefreshToken> RefreshTokens { get; set; }
        public virtual ICollection<Task> Tasks { get; set; }
    }
}

DbContext

Now let’s add the TasksDbContext that will inherit from the DbContext class of the EF Core:

C#
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using Microsoft.EntityFrameworkCore;

namespace TasksApi
{
    public partial class TasksDbContext : DbContext
    {
        public TasksDbContext()
        {
        }

        public TasksDbContext(DbContextOptions<TasksDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<RefreshToken> RefreshTokens { get; set; }
        public virtual DbSet<Task> Tasks { get; set; }
        public virtual DbSet<User> Users { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<RefreshToken>(entity =>
            {
               
                entity.Property(e => e.ExpiryDate).HasColumnType("smalldatetime");

                entity.Property(e => e.TokenHash)
                    .IsRequired()
                    .HasMaxLength(1000);

                entity.Property(e => e.TokenSalt)
                    .IsRequired()
                    .HasMaxLength(1000);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.HasOne(d => d.User)
                    .WithMany(p => p.RefreshTokens)
                    .HasForeignKey(d => d.UserId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_RefreshToken_User");

                entity.ToTable("RefreshToken");
            });

            modelBuilder.Entity<Task>(entity =>
            {               
                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(100);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.HasOne(d => d.User)
                    .WithMany(p => p.Tasks)
                    .HasForeignKey(d => d.UserId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Task_User");

                entity.ToTable("Task");
            });

            modelBuilder.Entity<User>(entity =>
            {
                entity.Property(e => e.Email)
                    .IsRequired()
                    .HasMaxLength(50);

                entity.Property(e => e.FirstName)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.LastName)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.Password)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.PasswordSalt)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.ToTable("User");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

And in your program.cs file, add the below just before the builder.build() call:

C#
builder.Services.AddDbContext<TasksDbContext>(options => options.UseSqlServer
(builder.Configuration.GetConnectionString("TasksDbConnectionString")));

Then in your appsettings.json, make sure to include the connection string for the database:

C#
{
  "ConnectionStrings": {
    "TasksDbConnectionString": "Server=Home\\SQLEXPRESS;Database=TasksDb;
                                Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Note: I have used the wonderful extension EF Core Power Tools which in a magical way it can translate a whole database structure and relationships into neat and proper DbContext entities and configurations.

You can install it from Extensions tab -> Manage Extensions of your Visual Studio 2022

Image 8

If you are following the design-first model for building your database first and then your EF Core mapping, I would highly recommend that you use this blazing fast and reliable tool to perform such operation, which will therefore boost your productivity and reduce the number of errors that might be introduced from manually creating the entities and configurations.

PasswordHashHelper

In order to save passwords on the database, we need to use secure hash HMAC 256 and salt from a secure random bytes of 256-bit sized, so we can protect the valuable users’ passwords from those nasty lurking thieves!

C#
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System.Security.Cryptography;

namespace TasksApi.Helpers
{
    public class PasswordHelper
    {
        public static byte[] GetSecureSalt()
        {
            // Starting .NET 6, the Class RNGCryptoServiceProvider is obsolete,
            // so now we have to use the RandomNumberGenerator Class 
            // to generate a secure random number bytes

            return RandomNumberGenerator.GetBytes(32);
        }
        public static string HashUsingPbkdf2(string password, byte[] salt)
        {
            byte[] derivedKey = KeyDerivation.Pbkdf2
            (password, salt, KeyDerivationPrf.HMACSHA256, iterationCount: 100000, 32);

            return Convert.ToBase64String(derivedKey);
        }
    }
}

We will also use these helper methods to save the refresh tokens on the database in a hashed format alongside their associated salts.

Implementing the JWT Authentication

Let’s add the needed JWT Bearer Package:

Image 9

Token Helper to Build Both Access Tokens and Refresh Tokens

Now let’s add the TokenHelper, which will include two methods to generate JWT-based access tokens and the other to generate a 32-byte based refresh tokens:

C#
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;

namespace TasksApi.Helpers
{
    public class TokenHelper
    {
        public const string Issuer = "http://codingsonata.com";
        public const string Audience = "http://codingsonata.com";
        public const string Secret = 
        "p0GXO6VuVZLRPef0tyO9jCqK4uZufDa6LP4n8Gj+8hQPB30f94pFiECAnPeMi5N6VT3/uscoGH7+zJrv4AuuPg==";
        public static async Task<string> GenerateAccessToken(int userId)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Convert.FromBase64String(Secret);

            var claimsIdentity = new ClaimsIdentity(new[] {
                new Claim(ClaimTypes.NameIdentifier, userId.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 securityToken = tokenHandler.CreateToken(tokenDescriptor);

            return await System.Threading.Tasks.Task.Run(() => 
                                          tokenHandler.WriteToken(securityToken));
        }
        public static async Task<string> GenerateRefreshToken()
        {
            var secureRandomBytes = new byte[32];

            using var randomNumberGenerator = RandomNumberGenerator.Create();
            await System.Threading.Tasks.Task.Run(() => 
                         randomNumberGenerator.GetBytes(secureRandomBytes));

            var refreshToken = Convert.ToBase64String(secureRandomBytes);
            return refreshToken;
        }
    }
}

Now let’s make sure to add the needed authentication and authorization middleware to the pipeline in program.cs file:

Add the below before the builder.build method:

C#
builder.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))
            };
        });

builder.Services.AddAuthorization();

And then before the app.run method, make sure the app will be using both middleware to authenticate and authorize your users:

C#
app.UseAuthentication();
app.UseAuthorization();

Requests and Responses

It is always advised that you accept and return structured objects instead of separated data, this is why we will prepare some Request and Response class that we will use them throughout our API:

Let’s add the below requests classes:

LoginRequest

C#
namespace TasksApi.Requests
{
    public class LoginRequest
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }
}

RefreshTokenRequest

C#
namespace TasksApi.Requests
{
    public class RefreshTokenRequest
    {
        public int UserId { get; set; }
        public string RefreshToken { get; set; }
    }
}

SignupRequest

C#
using System.ComponentModel.DataAnnotations;

namespace TasksApi.Requests
{
    public class SignupRequest
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
        [Required]
        public string ConfirmPassword { get; set; }
        [Required]
        public string FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
        [Required]
        public DateTime Ts { get; set; }
    }
}

TaskRequest

C#
namespace TasksApi.Requests
{
    public class TaskRequest
    {
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }
    }
}

Now let’s add the responses classes, these will be used to return the structured responses for the UI client calling the API:

BaseResponse

This will use a base class so other response classes can inherit from and extend their properties:

C#
using System.Text.Json.Serialization;

namespace TasksApi.Responses
{
    public abstract class BaseResponse
    {
        [JsonIgnore()]
        public bool Success { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string ErrorCode { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string Error { get; set; }
    }
}

DeleteTaskResponse

C#
using System.Text.Json.Serialization;

namespace TasksApi.Responses
{
    public class DeleteTaskResponse : BaseResponse
    {
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public int TaskId { get; set; }
    }
}

GetTasksResponse

C#
namespace TasksApi.Responses
{
    public class GetTasksResponse : BaseResponse
    {
        public List<Task> Tasks { get; set; }
    }
}

LogoutResponse

C#
namespace TasksApi.Responses
{
    public class LogoutResponse : BaseResponse
    {
    }
}

SaveTaskResponse

C#
namespace TasksApi.Responses
{
    public class SaveTaskResponse : BaseResponse
    {
        public Task Task { get; set; }
    }
}

SignupResponse

C#
namespace TasksApi.Responses
{
    public class SignupResponse : BaseResponse
    {
        public string Email { get; set; }
    }
}

TaskResponse

C#
namespace TasksApi.Responses
{
    public class TaskResponse
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }
    }
}

TokenResponse

C#
namespace TasksApi.Responses
{
    public class TokenResponse: BaseResponse
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
       
    }
}

ValidateRefreshTokenResponse

C#
namespace TasksApi.Responses
{
    public class ValidateRefreshTokenResponse : BaseResponse
    {
        public int UserId { get; set; }
    }
}

Interfaces

We will define three interfaces that will be implemented within the services. The interfaces are the abstractions that the Controllers would need to use to be able to process the related business logic and database calls, each interface would be implemented within a service which would be injected at runtime.

This is a very useful strategy (or design pattern) to make your API loosely coupled and easily testable.

ITokenService

C#
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface ITokenService
    {
        Task<Tuple<string, string>> GenerateTokensAsync(int userId);
        Task<ValidateRefreshTokenResponse> ValidateRefreshTokenAsync
                                           (RefreshTokenRequest refreshTokenRequest);
        Task<bool> RemoveRefreshTokenAsync(User user);
    }
}

IUserService

C#
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface IUserService
    {
        Task<TokenResponse> LoginAsync(LoginRequest loginRequest);
        Task<SignupResponse> SignupAsync(SignupRequest signupRequest);
        Task<LogoutResponse> LogoutAsync(int userId);
    }
}

ITasksInterface

C#
using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface ITaskService
    {
        Task<GetTasksResponse> GetTasks(int userId);
        Task<SaveTaskResponse> SaveTask(Task task);
        Task<DeleteTaskResponse> DeleteTask(int taskId, int userId);
    }
}

Services

Services act as the intermediate layer between your Controllers and your DbContext, it also includes any business related logic that the Controller should not bother about. Services implement the interfaces.

We will add three services:

TokenService

This will include methods to generate tokens, validate and remove refresh tokens:

C#
using Microsoft.EntityFrameworkCore;
using TasksApi.Helpers;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class TokenService : ITokenService
    {        
        private readonly TasksDbContext tasksDbContext;

        public TokenService(TasksDbContext tasksDbContext)
        {
            this.tasksDbContext = tasksDbContext;
        }

        public async Task<Tuple<string, string>> GenerateTokensAsync(int userId)
        {
            var accessToken = await TokenHelper.GenerateAccessToken(userId);
            var refreshToken = await TokenHelper.GenerateRefreshToken();

            var userRecord = await tasksDbContext.Users.Include
                             (o => o.RefreshTokens).FirstOrDefaultAsync(e => e.Id == userId);

            if (userRecord == null)
            {
                return null;
            }

            var salt = PasswordHelper.GetSecureSalt();

            var refreshTokenHashed = PasswordHelper.HashUsingPbkdf2(refreshToken, salt);

            if (userRecord.RefreshTokens != null && userRecord.RefreshTokens.Any())
            {
                await RemoveRefreshTokenAsync(userRecord);
            }
            userRecord.RefreshTokens?.Add(new RefreshToken
            {
                ExpiryDate = DateTime.Now.AddDays(30),
                Ts = DateTime.Now,
                UserId = userId,
                TokenHash = refreshTokenHashed,
                TokenSalt = Convert.ToBase64String(salt)
            });

            await tasksDbContext.SaveChangesAsync();

            var token = new Tuple<string, string>(accessToken, refreshToken);

            return token;
        }

        public async Task<bool> RemoveRefreshTokenAsync(User user)
        {
            var userRecord = await tasksDbContext.Users.Include
                (o => o.RefreshTokens).FirstOrDefaultAsync(e => e.Id == user.Id);

            if (userRecord == null)
            {
                return false;
            }

            if (userRecord.RefreshTokens != null && userRecord.RefreshTokens.Any())
            {
                var currentRefreshToken = userRecord.RefreshTokens.First();

                tasksDbContext.RefreshTokens.Remove(currentRefreshToken);
            }

            return false;
        }

        public async Task<ValidateRefreshTokenResponse> ValidateRefreshTokenAsync
                     (RefreshTokenRequest refreshTokenRequest)
        {
            var refreshToken = await tasksDbContext.RefreshTokens.FirstOrDefaultAsync
                               (o => o.UserId == refreshTokenRequest.UserId);

            var response = new ValidateRefreshTokenResponse();
            if (refreshToken == null)
            {
                response.Success = false;
                response.Error = "Invalid session or user is already logged out";
                response.ErrorCode = "R02";
                return response;
            }

            var refreshTokenToValidateHash = PasswordHelper.HashUsingPbkdf2
                                             (refreshTokenRequest.RefreshToken, 
                                             Convert.FromBase64String(refreshToken.TokenSalt));

            if (refreshToken.TokenHash != refreshTokenToValidateHash)
            {
                response.Success = false;
                response.Error = "Invalid refresh token";
                response.ErrorCode = "R03";
                return response;
            }
          
            if (refreshToken.ExpiryDate < DateTime.Now)
            {
                response.Success = false;
                response.Error = "Refresh token has expired";
                response.ErrorCode = "R04";
                return response;
            }

            response.Success = true;
            response.UserId = refreshToken.UserId;

            return response;
        }
    }
}

UserService

This will include methods related to login, logout and signup:

C#
using Microsoft.EntityFrameworkCore;
using TasksApi.Helpers;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class UserService : IUserService
    {
        private readonly TasksDbContext tasksDbContext;
        private readonly ITokenService tokenService;

        public UserService(TasksDbContext tasksDbContext, ITokenService tokenService)
        {
            this.tasksDbContext = tasksDbContext;
            this.tokenService = tokenService;
        }

        public async Task<TokenResponse> LoginAsync(LoginRequest loginRequest)
        {
            var user = tasksDbContext.Users.SingleOrDefault
                       (user => user.Active && user.Email == loginRequest.Email);

            if (user == null)
            {
                return new TokenResponse
                {
                    Success = false,
                    Error = "Email not found",
                    ErrorCode = "L02"
                };
            }
            var passwordHash = PasswordHelper.HashUsingPbkdf2
            (loginRequest.Password, Convert.FromBase64String(user.PasswordSalt));

            if (user.Password != passwordHash)
            {
                return new TokenResponse
                {
                    Success = false,
                    Error = "Invalid Password",
                    ErrorCode = "L03"
                };
            }

            var token = await System.Threading.Tasks.Task.Run(() => 
                        tokenService.GenerateTokensAsync(user.Id));

            return new TokenResponse
            {
                Success = true,
                AccessToken = token.Item1,
                RefreshToken = token.Item2
            };
        }

        public async Task<LogoutResponse> LogoutAsync(int userId)
        {
            var refreshToken = await tasksDbContext.RefreshTokens.FirstOrDefaultAsync
                               (o => o.UserId == userId);

            if (refreshToken == null)
            {
                return new LogoutResponse { Success = true };
            }

            tasksDbContext.RefreshTokens.Remove(refreshToken);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new LogoutResponse { Success = true };
            }

            return new LogoutResponse { Success = false, 
                                        Error = "Unable to logout user", ErrorCode = "L04" };
        }

        public async Task<SignupResponse> SignupAsync(SignupRequest signupRequest)
        {
            var existingUser = await tasksDbContext.Users.SingleOrDefaultAsync
                               (user => user.Email == signupRequest.Email);

            if (existingUser != null)
            {
                return new SignupResponse
                {
                    Success = false,
                    Error = "User already exists with the same email",
                    ErrorCode = "S02"
                };
            }

            if (signupRequest.Password != signupRequest.ConfirmPassword) {
                return new SignupResponse
                {
                    Success = false,
                    Error = "Password and confirm password do not match",
                    ErrorCode = "S03"
                };
            }

            if (signupRequest.Password.Length <= 7) // This can be more complicated than 
               // only length, you can check on alphanumeric and or special characters
            {
                return new SignupResponse
                {
                    Success = false,
                    Error = "Password is weak",
                    ErrorCode = "S04"
                };
            }

            var salt = PasswordHelper.GetSecureSalt();
            var passwordHash = PasswordHelper.HashUsingPbkdf2(signupRequest.Password, salt);

            var user = new User
            {
                Email = signupRequest.Email,
                Password = passwordHash,
                PasswordSalt = Convert.ToBase64String(salt),
                FirstName = signupRequest.FirstName,
                LastName = signupRequest.LastName,
                Ts = signupRequest.Ts,
                Active = true // You can save is false and send confirmation email 
                // to the user, then once the user confirms the email you can make it true
            };

            await tasksDbContext.Users.AddAsync(user);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new SignupResponse { Success = true, Email = user.Email };
            }

            return new SignupResponse
            {
                Success = false,
                Error = "Unable to save the user",
                ErrorCode = "S05"
            };
        }
    }
}

TaskService

This includes the methods for adding, removing and getting tasks:

C#
using Microsoft.EntityFrameworkCore;
using TasksApi.Interfaces;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class TaskService : ITaskService
    {
        private readonly TasksDbContext tasksDbContext;

        public TaskService(TasksDbContext tasksDbContext)
        {
            this.tasksDbContext = tasksDbContext;
        }

        public async Task<DeleteTaskResponse> DeleteTask(int taskId, int userId)
        {
            var task = await tasksDbContext.Tasks.FindAsync(taskId);

            if (task == null)
            {
                return new DeleteTaskResponse
                {
                    Success = false,
                    Error = "Task not found",
                    ErrorCode = "T01"
                };
            }

            if (task.UserId != userId)
            {
                return new DeleteTaskResponse
                {
                    Success = false,
                    Error = "You don't have access to delete this task",
                    ErrorCode = "T02"
                };
            }

            tasksDbContext.Tasks.Remove(task);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new DeleteTaskResponse
                {
                    Success = true,
                    TaskId = task.Id
                };
            }

            return new DeleteTaskResponse
            {
                Success = false,
                Error = "Unable to delete task",
                ErrorCode = "T03"
            };
        }

        public async Task<GetTasksResponse> GetTasks(int userId)
        {
            var tasks = await tasksDbContext.Tasks.Where
                        (o => o.UserId == userId).ToListAsync();

            if (tasks.Count == 0)
            {
                return new GetTasksResponse
                { 
                    Success = false, 
                    Error = "No tasks found for this user", 
                    ErrorCode = "T04"
                };
            }

            return new GetTasksResponse { Success = true, Tasks = tasks };
        }

        public async Task<SaveTaskResponse> SaveTask(Task task)
        {
            await tasksDbContext.Tasks.AddAsync(task);

            var saveResponse = await tasksDbContext.SaveChangesAsync();
            
            if (saveResponse >= 0)
            {
                return new SaveTaskResponse
                {
                    Success = true,
                    Task = task
                };
            }
            return new SaveTaskResponse
            {
                Success = false,
                Error = "Unable to save task",
                ErrorCode = "T05"
            };
        }
    }
}

Now, once you add those Interfaces and Tasks, let’s make sure that we configure them within the project’s builder pipeline:

C#
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddTransient<IUserService, UserService>();
builder.Services.AddTransient<ITaskService, TaskService>();

Controllers

Now it is the last part of the API, which is to build the endpoints that will be used by the users to access the backend resources:

First, we will make a new Controller that will inherit for the ControllerBase and inside it, we will have a small method and property to retrieve the logged in UserId, whenever the access token is provided, from the JWT-based access token claims:

So let’s add an API Controller, like the below:

BaseApiController

C#
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace TasksApi.Controllers
{
    public class BaseApiController : ControllerBase
    {
        protected int UserID => int.Parse(FindClaim(ClaimTypes.NameIdentifier));
        private string FindClaim(string claimName)
        {
            var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
            var claim = claimsIdentity.FindFirst(claimName);

            if (claim == null)
            {
                return null;
            }
            return claim.Value;
        }
    }
}

Now we can create our controllers that will inherit from this BaseApiController.

UsersController

Let’s start with the UsersController, it will include four methods: login, logout, signup and refresh the access token:

C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : BaseApiController
    {
        private readonly IUserService userService;
        private readonly ITokenService tokenService;

        public UsersController(IUserService userService, ITokenService tokenService)
        {
            this.userService = userService;
            this.tokenService = tokenService;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login(LoginRequest loginRequest)
        {
            if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Email) || 
                string.IsNullOrEmpty(loginRequest.Password))
            {
                return BadRequest(new TokenResponse
                {
                    Error = "Missing login details",
                    ErrorCode = "L01"
                });
            }

            var loginResponse = await userService.LoginAsync(loginRequest);

            if (!loginResponse.Success)
            {
                return Unauthorized(new
                {
                    loginResponse.ErrorCode,
                    loginResponse.Error
                });
            }

            return Ok(loginResponse);
        }

        [HttpPost]
        [Route("refresh_token")]
        public async Task<IActionResult> RefreshToken(RefreshTokenRequest refreshTokenRequest)
        {
            if (refreshTokenRequest == null || string.IsNullOrEmpty
            (refreshTokenRequest.RefreshToken) || refreshTokenRequest.UserId == 0)
            {
                return BadRequest(new TokenResponse
                {
                    Error = "Missing refresh token details",
                    ErrorCode = "R01"
                });
            }

            var validateRefreshTokenResponse = 
                await tokenService.ValidateRefreshTokenAsync(refreshTokenRequest);

            if (!validateRefreshTokenResponse.Success)
            {
                return UnprocessableEntity(validateRefreshTokenResponse);
            }

            var tokenResponse = await tokenService.GenerateTokensAsync
                                (validateRefreshTokenResponse.UserId);

            return Ok(new { AccessToken = tokenResponse.Item1, 
                            Refreshtoken = tokenResponse.Item2 });
        }

        [HttpPost]
        [Route("signup")]
        public async Task<IActionResult> Signup(SignupRequest signupRequest)
        {
            if (!ModelState.IsValid)
            {
                var errors = ModelState.Values.SelectMany
                             (x => x.Errors.Select(c => c.ErrorMessage)).ToList();
                if (errors.Any())
                {
                    return BadRequest(new TokenResponse
                    {
                        Error = $"{string.Join(",", errors)}",
                        ErrorCode = "S01"
                    });
                }
            }
         
            var signupResponse = await userService.SignupAsync(signupRequest);

            if (!signupResponse.Success)
            {
                return UnprocessableEntity(signupResponse);
            }

            return Ok(signupResponse.Email);
        }

        [Authorize]
        [HttpPost]
        [Route("logout")]
        public async Task<IActionResult> Logout()
        {
            var logout = await userService.LogoutAsync(UserID);

            if (!logout.Success)
            {
                return UnprocessableEntity(logout);
            }

            return Ok();
        }
    }
}

Note above that only the logout endpoint has the Authorize decoration, this is because we know that the user will be able to logout as long as he is logged in, which means he has a valid access token and refresh token.

TasksController

This includes all the endpoints that will allow the user to perform tasks-related operations, like getting all user’s tasks, saving and deleting the task for that user.

C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class TasksController : BaseApiController
    {
        private readonly ITaskService taskService;

        public TasksController(ITaskService taskService)
        {
            this.taskService = taskService;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var getTasksResponse = await taskService.GetTasks(UserID);

            if (!getTasksResponse.Success)
            {
                return UnprocessableEntity(getTasksResponse);
            }
            
            var tasksResponse = getTasksResponse.Tasks.ConvertAll(o => 
            new TaskResponse { Id = o.Id, IsCompleted = o.IsCompleted, 
                               Name = o.Name, Ts = o.Ts });

            return Ok(tasksResponse);
        }

        [HttpPost]
        public async Task<IActionResult> Post(TaskRequest taskRequest)
        {
            var task = new Task { IsCompleted = taskRequest.IsCompleted, 
            Ts = taskRequest.Ts, Name = taskRequest.Name, UserId = UserID };

            var saveTaskResponse = await taskService.SaveTask(task);

            if (!saveTaskResponse.Success)
            {
                return UnprocessableEntity(saveTaskResponse);
            }

            var taskResponse = new TaskResponse { Id = saveTaskResponse.Task.Id, 
            IsCompleted = saveTaskResponse.Task.IsCompleted, 
            Name = saveTaskResponse.Task.Name, Ts = saveTaskResponse.Task.Ts };
            
            return Ok(taskResponse);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var deleteTaskResponse = await taskService.DeleteTask(id, UserID);
            if (!deleteTaskResponse.Success)
            {
                return UnprocessableEntity(deleteTaskResponse);
            }

            return Ok(deleteTaskResponse.TaskId);
        }
    }
}

Note that the [Authorize] attribute is decorating the whole Controller since all these operations require an authenticated user with a valid access token.

Now we are done with the development part. Press F5 and check the browser is showing Swagger UI for your APIs.

Image 10

Testing on Postman

Now comes the QA part of testing our whole work to make sure everything is working fine and as per the requirements.

Of course, make sure you have the latest version of Postman installed and opened.

Let’s create a new collection and name it, Tasks Api.

First thing we need to test is the login endpoint since we have some test user already inserted in the database (included in the script part at the beginning of the tutorial).

Image 11

Let’s try to invalidate the email and see the result:

Image 12

Now let’s test the signup method:

Image 13

Take a look at the database User Table:

Image 14

Notice the 3rd record, the password was never saved in plain format, and the random salt was associated with it.

The access token usually would have a short duration, 10 or 15 minutes long, and once this is expired, you have to silently refresh the access token using the refresh token, which is much longer in duration, like 10 days or 3 weeks for example, and these tokens are sliding in time, so whenever you want to refresh and access token, you can just use the below endpoint to generate new pair of tokens.

refresh_token endpoint

Now let’s test the refresh token endpoint. You will need this endpoint to refresh the access token for the user after it becomes expired through any of the authorized API calls, such as the below:

Image 15

You will get 401 response, because the access token is no longer valid and you have to request for a new access token using the refresh token that you had from the first login.

Let’s try to refresh token:

Image 16

If we try to refresh the same token used before, it won’t work, simply because the refresh token triggers generating both new access token and new refresh token, so the previous refresh token would be invalidated (removed from the RefreshToken table on the database).

Image 17

Now let’s logout the user:

Image 18

Malicious or Already Logged Out User

Now let’s try this scenario, a valid user logs out of the system, but incidentally a malicious user has already obtained the refresh token for that user (in some way), and tries to refresh that user’s token so that he can gain unauthorized access, our APIs would guard our valid user by returning the below response for the malicious user.

This way, the malicious user cannot gain access to the valid user’s data.

Image 19

Let’s now do Get Tasks for the user:

Image 20

And let’s try adding a new task:

Image 21

and now let’s delete a task:

Image 22

Summary

Today, we have learned how to build a small simple tasks management system starting from the database using SQL Server Express, connecting it with ASP.NET Core Web API in .NET 6 and C# 10 within Visual Studio 2022. We mapped the database with EF Core (with the generous help of the great EF Core Power Tools) and then using the JWTBearer Nuget package, we managed to setup and implement the JWT-based authentication on the API project alongside applying the refresh tokens to make it even more practical for end users to get authenticated and authorized for the APIs without having to re-do the login process every 10 or 15 minutes.

If you think this tutorial is useful, please feel free to share it within your online network and with your colleagues. And don’t forget to subscribe to my blog to be notified once a new tutorial is posted.

Please let me know your thoughts or inquiries down below in the comments section.

References

You can find the source code for this tutorial in my GitHub account.

I have another tutorial that explains a little more about JWT, you can take a look sometime Secure ASP.NET Core Web API using JWT Authentication.

Feel free to check my other tutorials as well, I will make sure to update them to .NET 6 at the earliest:

Bonus

Please enjoy listening to this beautiful piano sonata.

Have a great day or night wherever you are, have fun listening and coding! and most importantly .. Please Stay Safe!

Mozart – Piano Sonata No. 8 in A minor, K. 310 (1st Movement)

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

 
SuggestionSuggest add a solution to Share jwt token among projects Pin
Lam Giang15-Apr-22 17:39
Lam Giang15-Apr-22 17:39 
GeneralMy vote of 5 Pin
FrodoFlee19-Feb-22 6:15
professionalFrodoFlee19-Feb-22 6:15 

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.