Learn Technologies
.NET Core 3.1 WEB API - JWT Refresh Token (part 2)

.NET Core 3.1 WEB API – JWT refresh Token (part 2)

In the last posts, we saw how to setup .Net Core Web API 3.1 project with Entity Framework and secure Web API by JWT Tokens . If you haven’t seen it yet, I invite you to read it through this links:

In this new tutorial, we’ll see how to make JWT refresh token mechanism.

Why we need refresh token?

In fact, JWT token has a lifetime, and we are interested in changing it periodically to make it very difficult for a hacker to capture the login password. So the objective is to minimize the sending of login and password through the network.

You can see the video step by step on my YouTube channel.

Add RefreshToken Table

First of all, let’s add new model called RefreshTokenModel which contains 2 properties Email and RefreshToken type of string.

public class RefreshTokenModel: BaseModel
{
        public string Email { get; set; }

        public string RefreshToken { get; set; }
}

After that, add the this model like a DbSet in the RefreshTokenContext:

 public DbSet<RefreshTokenModel> RefreshTokens { get; set; }

Now, let’s add the TokensRepository and TokensManager. Like in the previous posts, we’ll use them like dependency.

This manager will contain essentially, 3 methods AddToken, UpdateToken and GetToken:

public class UsersManager : BaseManager, IUsersManager
    {
        private readonly IUsersRepository usersRepository;

        public UsersManager(IUnitOfWork unitOfWork, IUsersRepository usersRepository)
            : base(unitOfWork)
        {
            this.usersRepository = usersRepository;
        }

        public void Delete(long id)
        {
            usersRepository.Delete(id);
            UnitOfWork.Commit();
        }

        public IEnumerable<UserModel> GetAllUsers()
        {
            return usersRepository.Get(x => x.DeleteDate == null).WithoutPassword();
        }

        public UserModel GetUserByEmail(string email)
        {
            return usersRepository.Get(x => x.Email == email && x.DeleteDate == null).SingleOrDefault().WithoutPassword();
        }

        public long Insert(UserModel userModel)
        {
            userModel.CreationDate = userModel.ModificationDate = DateTime.UtcNow;
            userModel.Status = StatusEnum.Pending;
            usersRepository.Insert(userModel);
            UnitOfWork.Commit();
            return userModel.ID;

        }

        public UserModel Login(LoginDto loginDto)
        {
            if (loginDto == null || string.IsNullOrEmpty(loginDto.Email) || string.IsNullOrEmpty(loginDto.Password))
            {
                return null;
            }

            return usersRepository.Get(x => x.DeleteDate == null && x.Email == loginDto.Email && x.Password == loginDto.Password).SingleOrDefault().WithoutPassword();
        }

        public UserModel Update(UserModel userModel)
        {
            var targetUser = usersRepository.Get(x => x.DeleteDate == null && x.Email == userModel.Email).SingleOrDefault();

            if (targetUser != null)
            {
                userModel.ModificationDate = DateTime.UtcNow;
                userModel.Password = targetUser.Password;
                usersRepository.Update(userModel);
                UnitOfWork.Commit();
                return userModel.WithoutPassword();
            }
            return null;
        }
    }

Finally, don’t forget to add both(Manager and repository ) in startup file like a service.

//....
 services.AddScoped<IUnitOfWork, UnitOfWork>();
 services.AddScoped<IUsersRepository, UsersRepository>();
 services.AddScoped<IRefreshTokensRepository, RefreshTokensRepository>();

 services.AddScoped<IUsersManager, UsersManager>();
 services.AddScoped<IRefreshTokensManager, RefreshTokensManager>();
 services.Configure<AppSettings>(appSettingsSection);

Now, let’s move to the UsersConroller and add some changes. We need to modify the login action and creating new action called RefreshToken.

First of all, in the login action, we verify whether the refresh table contains already an item with the given user email. If it’s the case we update the target item existed already else we create a new item.

After that, we add a the refresh action, in which we verify whether the given refresh token exists in the database and it match the given user email. In other way, we return bad request response.

So, you code will look like:

  [AllowAnonymous]
        [HttpPost("Login")]
        public IActionResult Login([FromBody] LoginDto loginDto)
        {
            UserModel userModel = usersManager.Login(loginDto);
            if (userModel == null)
            {
                return BadRequest("Bad login or password.");
            }

            // JWT Tokens
            var now = DateTime.UtcNow;
            var refreshTokenModel = refreshTokensManager.GetToken(userModel.Email);

            var tokenHandler = new JwtSecurityTokenHandler();
            var secretKey = Encoding.ASCII.GetBytes(appSettings.SecretKey);
            var tokenDiscriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new Claim[] {
                    new Claim(ClaimTypes.Email, userModel.Email)
                }),
                Expires = now.AddMinutes(4),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature)
            };

            var token = tokenHandler.CreateToken(tokenDiscriptor);
            userModel.Token = tokenHandler.WriteToken(token);

            if(refreshTokenModel != null)
            {
                refreshTokenModel = refreshTokensManager.UpdateToken(refreshTokenModel.RefreshToken);
            }
            else
            {
                refreshTokenModel = refreshTokensManager.AddToken(userModel.Email);
            }

            userModel.RefreshToken = refreshTokenModel.RefreshToken;
            userModel.TokenExpires = now.AddMinutes(4);

            return Ok(userModel);
        }

        [AllowAnonymous]
        [HttpPost("RefreshToken")]
        public IActionResult RefreshToken([FromBody] string refreshToken)
        {
            var refreshTokenModel = refreshTokensManager.GetToken(refreshToken);
            var userModel = usersManager.GetUserByEmail(refreshTokenModel.Email);
            DateTime now = DateTime.UtcNow;

            if (refreshTokenModel == null || userModel == null)
            {
                return BadRequest("Bad Token.");
            }

            var tokenHandler = new JwtSecurityTokenHandler();
            var secretKey = Encoding.ASCII.GetBytes(appSettings.SecretKey);
            var tokenDiscriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new Claim[] {
                    new Claim(ClaimTypes.Email, userModel.Email)
                }),
                Expires = now.AddMinutes(4),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature)
            };

            var token = tokenHandler.CreateToken(tokenDiscriptor);
            userModel.Token = tokenHandler.WriteToken(token);
            refreshTokenModel = refreshTokensManager.UpdateToken(refreshTokenModel.RefreshToken);
            userModel.RefreshToken = refreshTokenModel.RefreshToken;
            userModel.TokenExpires = now.AddMinutes(4);

            return Ok(userModel);
        }

Now, let’s go to postman and run some queries. In order to get tokens, we run the query is Login.

After that, any query has to include the bearer token in the header. Once you get 401 unauthorized error, you have to run the refresh token action with the refresh token string generated in login operation.

In conclusion, refresh token mechanism is very important to protect the web API. However, a bad implementation can expose your site to attacks.

Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x