Menu Close

Using Refresh token in JWT Authentication


In this article we will discuss about in details about Using Refresh token in JWT Authentication. Here we discussed how refresh token use in JWT Authentications and why refresh token is necessary to use in JWT. Here I have taken VS 2022 with .NET Core 6.0. Please read my previous article Using API Key Authentication To Secure ASP.NET Core Web API.

What is JWT Token Authentication?

JSON Web Token (JWT) is a JSON encoded representation of a claim(s) that can be transferred between two parties. The claim is digitally signed by the issuer of the token, and the party receiving this token can later use this digital signature to prove the ownership on the claim.

JWTs can be broken down into three parts: header, payload, and signature. Each part is separated from the other by dot (.), and will follow the below structure:

Header.Payload.Signature

Why we need Refresh Token in JWT?

Refresh tokens are the kind of tokens that can be used to get new access tokens. When the access tokens expire, we can use refresh tokens to get a new access token from the authentication controller. The lifetime of a refresh token is usually much longer compared to the lifetime of an access token.

In the below image it describe the better way of Refresh Token. Please follow using numbering that show in image

  1. In the client User is Login using the credentials and it request to server.
  2. In the server it authenticate the credentials and create a JWT secret Key.
  3. After successfully authenticate it provides a refresh token using user information.
  4. If you can access the request with Expired token then the JWT don’t validate and it throws status as 401 and with token expired message.
  5. If you can call api/auth/refreshtoken then it create a new JWT token header and it can access the request.
JWT-Refresh-Token
If we are using an access token for a long time, there is a chance a hacker can steal our token and misuse it. Hence it is not very safe to use the access token for a long period.

Creating ASP.NET Core Web API in .Net 6.0

Here I am using Visual Studio 2022 to create .NET 6.0 applications. We can choose ASP.NET Core Web API template from Visual Studio 2022 like below.

Adding below required packages using Nuget

Add the below required packages using Nuget,

Microsoft.EntityFrameworkCore.SqlServer 
Microsoft.EntityFrameworkCore.Tools 
Microsoft.AspNetCore.Identity.EntityFrameworkCore 
Microsoft.AspNetCore.Authentication.JwtBearer 

Adding configuration details in appsettings.json

Open appsettings.json file and add the below configuration like this. Here I add two configuration

  • Added connection string details
  • Added JWTAuth configuration details
"ConnectionStrings": {
    "ConnStr": "Data Source=MSCNUR1888004;Initial Catalog=JWTRefreshToken;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "JWTAuth": {
    "ValidAudienceURL": "http://localhost:4200",
    "ValidIssuerURL": "http://localhost:5000",
    "SecretKey": "%TrwmbxYunsJWT8972jbgsjjfsnHHTwwmkOOmNVDRNBSTmmdshgsynsnb%[email protected]&",
    "TokenValidityInMinutes": 1,
    "RefreshTokenValidityInDays": 7
  }

We have given database connection string and a few other configuration values for JWT authentication in the above appsettings.json. We have given only 1 minute for access token expiration time and 7 days for refresh token expiry time. (You can change these configurations as per your requirements).

Creating DBContext to communicate Database

We can create a new folder “Context” and create a class AppDbContext class under Context folder and add below code.

public class ApplicationDbContext : IdentityDbContext<IdentityUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }

Creating Required Model class

On below I have created the required model classes.

Register Model

 public class Register
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [EmailAddress]
        [Required(ErrorMessage = "Email is required")]
        public string? Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }

Login Model

public class Login
    {
        [Required(ErrorMessage = "User Name is required")]
        public string? Username { get; set; }

        [Required(ErrorMessage = "Password is required")]
        public string? Password { get; set; }
    }

User Roles

We have added two constant values “Admin” and “User” as roles. You can add as many roles as you wish.

public static class UserRoles
    {
        public const string Admin = "Admin";
        public const string User = "User";
    }

Response

Response class is for returning the response value after user registration and user login. It will also return error messages if the request fails. 

public class Response
    {
        public string? Status { get; set; }
        public string? Message { get; set; }
    }

Token Model

We can create a class “TokenModel” which will be used to pass access token and refresh token into the refresh method of the authenticate controller.

public class TokenModel
    {
        public string? AccessToken { get; set; }
        public string? RefreshToken { get; set; }
    }

Application User

Here we have extended default Identity user with new properties refresh token and refresh token expiry time.

using Microsoft.AspNetCore.Identity;

namespace JWTAuthRefreshToken.Auth
{
    public class ApplicationUser : IdentityUser
    {
        public string? RefreshToken { get; set; }
        public DateTime RefreshTokenExpiryTime { get; set; }
    }
}

Adding Auth Controller file

Let’s create an API controller “AuthController” inside the “Controllers” folder.

  • We have added three methods loginregister, and register-admin inside the controller class. Register and register-admin are almost same, but the register-admin method will be used to create a user with admin role. In login method, we have returned a JWT token after successful login.
  • Here I have used Identity mechanism to get manage user roles and model.
  • GetToken is used to create JWT token using our security keys that we defined in appsettings.json file.
using JWTAuthentication.NET6._0.Auth;
using JWTAuthRefreshToken.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;

namespace JWTAuthRefreshToken.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        
        private readonly IConfiguration _configuration;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AuthController(
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IConfiguration configuration)
        {
            _userManager = userManager;
            _roleManager = roleManager;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login([FromBody] LoginModel model)
        {
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
            {
                var userRoles = await _userManager.GetRolesAsync(user);

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                };

                foreach (var userRole in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, userRole));
                }

                var token = CreateToken(authClaims);
                var refreshToken = GenerateRefreshToken();

                _ = int.TryParse(_configuration["JWTAuth:RefreshTokenValidityInDays"], out int refreshTokenValidityInDays);

                user.RefreshToken = refreshToken;
                user.RefreshTokenExpiryTime = DateTime.Now.AddDays(refreshTokenValidityInDays);

                await _userManager.UpdateAsync(user);

                return Ok(new
                {
                    Token = new JwtSecurityTokenHandler().WriteToken(token),
                    RefreshToken = refreshToken,
                    Expiration = token.ValidTo
                });
            }
            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        [HttpPost]
        [Route("register-admin")]
        public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)
        {
            var userExists = await _userManager.FindByNameAsync(model.Username);
            if (userExists != null)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });

            if (!await _roleManager.RoleExistsAsync(UserRoles.Admin))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));
            if (!await _roleManager.RoleExistsAsync(UserRoles.User))
                await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));

            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.Admin);
            }
            if (await _roleManager.RoleExistsAsync(UserRoles.Admin))
            {
                await _userManager.AddToRoleAsync(user, UserRoles.User);
            }
            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }

        [HttpPost]
        [Route("refresh-token")]
        public async Task<IActionResult> RefreshToken(TokenModel tokenModel)
        {
            if (tokenModel is null)
            {
                return BadRequest("Invalid client request");
            }

            string? accessToken = tokenModel.AccessToken;
            string? refreshToken = tokenModel.RefreshToken;

            var principal = GetPrincipalFromExpiredToken(accessToken);
            if (principal == null)
            {
                return BadRequest("Invalid access token or refresh token");
            }

#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8602 // Dereference of a possibly null reference.
            string username = principal.Identity.Name;
#pragma warning restore CS8602 // Dereference of a possibly null reference.
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.

            var user = await _userManager.FindByNameAsync(username);

            if (user == null || user.RefreshToken != refreshToken || user.RefreshTokenExpiryTime <= DateTime.Now)
            {
                return BadRequest("Invalid access token or refresh token");
            }

            var newAccessToken = CreateToken(principal.Claims.ToList());
            var newRefreshToken = GenerateRefreshToken();

            user.RefreshToken = newRefreshToken;
            await _userManager.UpdateAsync(user);

            return new ObjectResult(new
            {
                accessToken = new JwtSecurityTokenHandler().WriteToken(newAccessToken),
                refreshToken = newRefreshToken
            });
        }

        [Authorize]
        [HttpPost]
        [Route("revoke/{username}")]
        public async Task<IActionResult> Revoke(string username)
        {
            var user = await _userManager.FindByNameAsync(username);
            if (user == null) return BadRequest("Invalid user name");

            user.RefreshToken = null;
            await _userManager.UpdateAsync(user);

            return NoContent();
        }

        [Authorize]
        [HttpPost]
        [Route("revoke-all")]
        public async Task<IActionResult> RevokeAll()
        {
            var users = _userManager.Users.ToList();
            foreach (var user in users)
            {
                user.RefreshToken = null;
                await _userManager.UpdateAsync(user);
            }

            return NoContent();
        }

        private JwtSecurityToken CreateToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWTAuth:SecretKey"]));
            _ = int.TryParse(_configuration["JWTAuth:TokenValidityInMinutes"], out int tokenValidityInMinutes);

            var token = new JwtSecurityToken(
                issuer: _configuration["JWTAuth:ValidIssuerURL"],
                audience: _configuration["JWTAuth:ValidAudienceURL"],
                expires: DateTime.Now.AddMinutes(tokenValidityInMinutes),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                );

            return token;
        }

        private static string GenerateRefreshToken()
        {
            var randomNumber = new byte[64];
            using var rng = RandomNumberGenerator.Create();
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }

        private ClaimsPrincipal? GetPrincipalFromExpiredToken(string? token)
        {
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])),
                ValidateLifetime = false
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
            if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
                throw new SecurityTokenException("Invalid token");

            return principal;

        }
    }
}
  • In the login method, we create an access token and refresh token and return to the response of the request.
  • In the refresh method, we are checking the expired access token and existing token and if both are confirmed correctly then a new access token and refresh token generate and return to the response.
  • We have two revoke methods implemented inside the authenticate controller. One method is used to revoke a refresh token for a particular user and the other method is used to revoke refresh token for entire user inside the database.

Program

We must create a database and tables needed before running the application. As we are using entity framework, we can use the below database migration command with package manger console to create a migration script.

using JWTAuthentication.NET6._0.Auth;
using JWTAuthRefreshToken.Auth;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;
// Add services to the container.

builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

// For Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Adding Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ClockSkew = TimeSpan.Zero,

        ValidAudience = configuration["JWTAuth:ValidAudienceURL"],
        ValidIssuer = configuration["JWTAuth:ValidIssuerURL"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWTAuth:SecretKey"]))
    };
});

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

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();

Run commands to add into Database

Let’s run the below command to add the migrations model. Click on Tools then open Package Manager Console.

add-migration Initial

You can see after running this commend it created a migrations folder that hold our model changes.

Then we should run the below command, this will updated into database holding the migrations object

update-database

After run the above command you check the database using SQL server object explorer, you can see that the database is created with tables.

ASP.NET Core Identity is a membership system which allows you to add login functionality to your application. Users can create an account and login with a username and password, or they can use an external login provider such as Facebook, Google, Microsoft Account, Twitter and more. 

You can configure ASP.NET Core Identity to use a SQL Server database to store usernames, passwords, and profile data. Alternatively, you can use your own persistent store to store data in another other persistent storage, such as Azure Table Storage. 

We can add “Authorize” attribute inside the WeatherForecast controller. 

Testing JWT Refresh Token in POSTMAN

Register the user

Register the user using below API call it created a user.

If you look at the user table inside the database, you can see that new user created and refresh token is null and refresh token expiry time is also null.

Login with current user credentials

If you look at the user table again, you can see that refresh token and token expiry time is now updated with current values we received after login method.

Get Request WeatherForecast API

After Token Expired What happen

You may notice that we have given only 1 minute as token expiry time. After one minute, if you again try to access weatherforecast controller, you get 401 un-authorized error.

Calling Refresh Token

Now we can use our current access token and refresh token to generate new access token and refresh token using ”refresh-token” method inside the authenticate controller.

After refreshing the access token and refresh token we get a new access token and refresh token. We can use this new access token within one minute to access the secured weatherforecast controller. Previously used tokens became invalid now.

If you try to refresh using existing access token and refresh token, you will get an invalid token error message.

You can use the revoke method to revoke refresh token for a particular user or all users.

Using the above method, we have revoked the refresh token for a specific user. If you check the user table, you can see that the refresh token is null now.

If we are using any client applications (Angular / React / Vue) we can keep these access tokens and refresh tokens inside the local storage and we can handle the requests using route guards.

#Source Code

Conclusion

In this post, we have seen how to use refresh token along with JWT access tokens to secure our .NET Core 6.0 Web API application. Refresh tokens are extremely useful to ensure more application security. We usually give small expiration time for access tokens and after expiration, we use refresh tokens to get new access tokens. Hence, if any attacker gets this access token, they can’t use it for longer time. We also supply a revoke method to revoke refresh token for a specific user or all users. So that it will give more security to the application. Please write to me if have any concerns or having any problem with this post.

Jayant Tripathy
Coder, Blogger, YouTuber

A passionate developer keep focus on learning and working on new technology.


Leave a Reply

Your email address will not be published.