JWT Refresh Token in ASP.NET CORE C# [Authentication Detail Guide]

/ / 10 Comments

ASP.NET Core Refresh JWT Token C#: Here in this article will see how can we refresh JWT Token in Asp.Net Core Web API, once the access token is expired. And try to understand how the refresh JWT token works with the flow diagram.

In a recent article, we discussed how to implement JWT Token Authentication in Asp.net Core C# in a straightforward way.

This article is an advanced version of it, here we re-generate the new JWT access token using the refresh token in Asp.net Core 5 web API project.

The ideal flow of JWT Authentication is we first authenticate the user by passing a username and password. Once the user is authenticated we generate a JWT token and return it to the client.

The client passes this newly generated JWT access token in every request header as Bearer JWT Token to consume our other secured APIS. 

Steps to get JWT access token using refresh token [C#]

  • Understanding Refresh JWT Token Workflow in Asp.net Core.
  • Create an Asp.net Core Web API project and Install the Nuget package (JwtBearer)
  • Modify the Appsetting.json file for the JWT configuration setting
  • Modify Startup.cs configure services add Authentication and JwtBearer configuration
  • Create Models User, Tokens, UserRefreshTokens
  • Add AppDbContext class file for Database configuration, transaction
  • Using EF Core adds a new table UserRefreshTokens 
  • Add the JWTManagerRepository class responsible to create the JWT access token
  • Add UserServiceRepository class responsible to access, and saving refresh token to DB
  • Add UsersController - Authenticate, RefreshToken action method.

# Understanding Refresh JWT Token Workflow in ASP.NET Core.

Here we understand in detail how JWT refresh tokens work in Asp.Net Core C#. As a best practice, we always keep the JWT expiration for a few minutes .i.e 5,10 mins.

And once the token gets expired, the client is no longer able to consume our secured API. And to overcome this problem client has two options as mentioned below:

  • Option 1 after token expiration, the user re-login by passing a username and password and gets the new access token.
  • Option 2 uses the Refresh token re-generate a new JWT access token and consume the secured API (without re-login).

Using Option 1 to handle JWT token expiration is not an ideal approach. As re-login user results in a lousy user experience, hence the refresh token comes to the rescue i.e. Option 2.

The below image demonstrates the workflow to get a new JWT access token using a refresh token in ASP.NET CORE C#.

Asp.net Core + JWT Refresh Token workflow diagram

# Create an Asp.net Core Web API project and Install the Nuget package (JwtBearer)

Here first we create the Asp.net Core Web API project using the ASP.NET CORE 5 version if you want you can also select a lower version i.e ASP.NET CORE 3.1 Version.

Now we installed some NuGet packages using Visual Studio Nuget Package Manager UI i.e Tools --> NuGet Package Manager --> Manage NuGet Packages for Solution.

Below are listed the required NuGet packages we need to implement JWT authentication in the ASP.NET CORE application.

Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
System.IdentityModel.Tokens.Jwt

# Modify Appsetting.json file for JWT configuration setting

Now we have to modify our appsetting.json file and add the JWT secret key, Issuer, and Audience values. These values we required while generating JSON Web Token. Also, we add a connection string, which connects to our database.

Our final appsetting.json file looks like as below:

 "ConnectionStrings": {
    "SqlServerDbCon": "Server=localhost;Database=iTest;Trusted_Connection=True;"
  },
  "JWT": {
    "Key": "This is my supper secret key for jwt",
    "Issuer": "https://codepedia.info",
    "Audience": "codepedia.info"
  }

Note: Here am using a database like MS SQL, and adding connection string accordingly. You are free to choose any other RDMS (PostgreSQL, MySQL, Oracle, etc) and add connection strings respectively.


# Modify Startup.cs configure services add Authentication and JwtBearer configuration.

In startup.cs file under the ConfigureServices method first we configure database connectivity using services.AddDbContext(). Next, we add services.AddIdentity() which configures the Identity system for users and role types.

And last but not least we add services.AddAuthentication() and AddJwtBearer(), which enables JWT Bearer Authentication in our ASP.NET Core application.

Our final code looks like as written below:

public void ConfigureServices(IServiceCollection services)
{
	services.AddDbContext<AppDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SqlServerDbCon")));

	services.AddIdentity<IdentityUser, IdentityRole>(options =>	{
		options.Password.RequireUppercase = true; // on production add more secured options
		options.Password.RequireDigit = true;               
		options.SignIn.RequireConfirmedEmail = true;
	}).AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders();

	services.AddAuthentication(x => {
		x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
		x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
	}).AddJwtBearer(o => {
		var Key = Encoding.UTF8.GetBytes(Configuration["JWT:Key"]);
		o.SaveToken = true;
		o.TokenValidationParameters = new TokenValidationParameters {
			ValidateIssuer = false, // on production make it true
			ValidateAudience = false, // on production make it true
			ValidateLifetime = true,
			ValidateIssuerSigningKey = true,
			ValidIssuer = Configuration["JWT:Issuer"],
			ValidAudience = Configuration["JWT:Audience"],
			IssuerSigningKey = new SymmetricSecurityKey(Key),
			ClockSkew = TimeSpan.Zero
		};
		o.Events = new JwtBearerEvents {
			OnAuthenticationFailed = context => {
				if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
				{
					context.Response.Headers.Add("IS-TOKEN-EXPIRED", "true");
				}
				return Task.CompletedTask;
			}
		};
	});

	services.AddSingleton<IJWTManagerRepository, JWTManagerRepository>();
	services.AddScoped<IUserServiceRepository, UserServiceRepository>();	
	services.AddControllers();
}

Here in AddJwtBearer(), we added an event that let us know if the JWT Access Token is expired. And in the response header, we set IS-TOKEN-EXPIRED as true, which helps us to call the refresh IAction method from the client side.


Also using Configuration["JWT:Key"] we get the JWT secret key from our appsetting.json file. 

Our Configure method code looks like as written below:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{	
    // your other middleware... 
	app.UseRouting();
	app.UseAuthentication(); // This need to be added before UseAuthorization()	
	app.UseAuthorization();
	app.UseEndpoints(endpoints =>
	{
	    endpoints.MapControllers();
	});
}

# Create Models User, Tokens, UserRefreshTokens

Now we add a new folder named Models and add three new classes .i.e Users, Tokens, and UserRefreshTokens. Our respective classes file codes look as written below.

Users.cs file code:

public class Users
{
	public string Name { get; set; }
	public string Password { get; set; }
}

User class has two properties i.e Name, and Password. We use this class in our UserController action method for authentication.

Tokens.cs file code:

public class Tokens
{
	public string Access_Token { get; set; }
	public string Refresh_Token { get; set; }
}

The tokens class has two string properties i.e Access_Token, and Refresh_Token.

UserRefreshTokens.cs file code:

public class UserRefreshTokens
{
	[Key]
	public int Id { get; set; }
	[Required]
	public string UserName { get; set; }
	[Required]
	public string RefreshToken { get; set; }
	public bool IsActive { get; set; } = true;
} 

This class represents the UserRefreshTokens table, which saves refresh tokens against valid users.

Note: We need to import the System.ComponentModel.DataAnnotations in UserRefreshTokens.cs 

# Add AppDbContext class file for Database configuration, transaction

Now we add a new class file AppDbContext.cs under our Models folder. And add the below-written code inside our AppDbContext file.

public class AppDbContext : IdentityDbContext<IdentityUser>
{
	public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
	{	  
	
	}
	
	public virtual DbSet<UserRefreshTokens> UserRefreshToken { get; set; }
}

# Using EF Core add a new table UserRefreshTokens 

As in Step 2, we have installed all the mentioned EntityFrameworkCore packages, and also we are done with the DBcontext file and database connection.

Now with the below command, our Identity tables and UserRefreshTokens tables get created.

add-migration dbinitials
update-database

As our primary focus is on the JWT token, so am not going into detail on EF CORE and DB context.

# Add JWTManagerRepository class responsible to create the JWT access token

First, we create a new folder named Repository and add a new interface as IJWTManagerRepository.

In the interface IJWTManagerRepository, we added 3 methods. Our IJWTManagerRepository.cs file code looks like as written below:

public interface IJWTManagerRepository
{
	Tokens GenerateToken(string userName);
	Tokens GenerateRefreshToken(string userName);
	ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
}

Here GenerateToken and GenerateRefreshToken methods return valid JWT access-token and refresh-token. And GetPrincipalFromExpiredToken method returns ClaimsPrincipal from the expired JWT access token.

Now add a new class JWTManagerRepository and inherit interface IJWTManagerRepository.

Our implementation code looks like this as written below:

public class JWTManagerRepository : IJWTManagerRepository
{        
	private readonly IConfiguration iconfiguration;

	public JWTManagerRepository(IConfiguration iconfiguration)
	{
		this.iconfiguration = iconfiguration;
	}
	public Tokens GenerateToken(string userName)
	{
		return GenerateJWTTokens(userName);
	}

	public Tokens GenerateRefreshToken(string username)
	{
		return GenerateJWTTokens(username);
	}

	public Tokens GenerateJWTTokens(string userName)
	{
		try
		{
			var tokenHandler = new JwtSecurityTokenHandler();
			var tokenKey = Encoding.UTF8.GetBytes(iconfiguration["JWT:Key"]);
			var tokenDescriptor = new SecurityTokenDescriptor
			{
				Subject = new ClaimsIdentity(new Claim[]
			  {
				 new Claim(ClaimTypes.Name, userName)
			  }),
				Expires = DateTime.Now.AddMinutes(1),
				SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(tokenKey), SecurityAlgorithms.HmacSha256Signature)
			};
			var token = tokenHandler.CreateToken(tokenDescriptor);
			var refreshToken = GenerateRefreshToken();
			return new Tokens { Access_Token = tokenHandler.WriteToken(token), Refresh_Token = refreshToken };
		}
		catch (Exception ex)
		{
			return null;
		}
	}

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

	public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
	{
		var Key = Encoding.UTF8.GetBytes(iconfiguration["JWT:Key"]);

		var tokenValidationParameters = new TokenValidationParameters
		{
			ValidateIssuer = false,
			ValidateAudience = false,
			ValidateLifetime = false,
			ValidateIssuerSigningKey = true,
			IssuerSigningKey = new SymmetricSecurityKey(Key),
			ClockSkew = TimeSpan.Zero
		};

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


		return principal;
	}            
   
}

#Add UserServiceRepository class responsible to access, and saving refresh token to DB

Now we add a new interface as IUserServiceRepository.cs which has all user-related methods. Our IUserServiceRepository.cs interface code looks like this as written below:

public interface IUserServiceRepository
{
	Task<bool> IsValidUserAsync(Users users);
	
	UserRefreshTokens AddUserRefreshTokens(UserRefreshTokens user);

	UserRefreshTokens GetSavedRefreshTokens(string username, string refreshtoken);

	void DeleteUserRefreshTokens(string username, string refreshToken);

	int SaveCommit();
}

Here IsValidUserAsync() method returns true if the user is valid and active in our user table database.

AddUserRefreshTokens() and DeleteUserRefreshTokens() methods are used to add and delete refresh-token into the database.

Now add a new class UserServiceRepository.cs and inherit IUserServiceRepository.cs interface.

Our UserServiceRepository.cs class file code looks like this as written below:

public class UserServiceRepository : IUserServiceRepository
{
	private readonly UserManager<IdentityUser> _userManager;
	private readonly AppDbContext _db;

	public UserServiceRepository(UserManager<IdentityUser> userManager, AppDbContext db)
	{
		this._userManager = userManager;
		this._db = db;
	}

	public UserRefreshTokens AddUserRefreshTokens(UserRefreshTokens user)
	{
		_db.UserRefreshToken.Add(user);
		return user;
	}

	public void DeleteUserRefreshTokens(string username, string refreshToken)
	{
		var item = _db.UserRefreshToken.FirstOrDefault(x => x.UserName == username && x.RefreshToken == refreshToken);
		if (item != null)
		{
			_db.UserRefreshToken.Remove(item);
		}
	}
	
	public UserRefreshTokens GetSavedRefreshTokens(string username, string refreshToken)
	{
		return _db.UserRefreshToken.FirstOrDefault(x => x.UserName == username && x.RefreshToken == refreshToken && x.IsActive == true);
	}

	public int SaveCommit()
	{
		return _db.SaveChanges();
	}
	
	public async Task<bool> IsValidUserAsync(Users users)
	{
		var u = _userManager.Users.FirstOrDefault(o => o.UserName == users.Name);
		var result = await _userManager.CheckPasswordAsync(u, users.Password);
		return result;

	}
}

# Add UsersController - Authenticate, RefreshToken action method.

Now we add a new controller named as UsersController and in its constructor, we inject IJWTManagerRepository and the IUserServiceRepository interface.

The UsersController has 2 POST methods and 1 GET IAction method.

Our UsersController code looks like this as written below:

[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
	private readonly IJWTManagerRepository jWTManager;
	private readonly IUserServiceRepository userServiceRepository;

	public UsersController(IJWTManagerRepository jWTManager, IUserServiceRepository userServiceRepository)
	{
		this.jWTManager = jWTManager;
		this.userServiceRepository = userServiceRepository;
	}

	[HttpGet]
	public List<string> Get()
	{
		var users = new List<string>
		{
			"Satinder Singh",
			"Amit Sarna",
			"Davin Jon"
		};

		return users;
	}

	[AllowAnonymous]
	[HttpPost]
	[Route("authenticate")]
	public async Task<IActionResult> AuthenticateAsync(Users usersdata)
	{
		var validUser = await userServiceRepository.IsValidUserAsync(usersdata);

		if (!validUser)
		{
			return Unauthorized("Incorrect username or password!");
		}

		var token = jWTManager.GenerateToken(usersdata.Name);

		if (token == null)
		{
			return Unauthorized("Invalid Attempt!");
		}

		// saving refresh token to the db
		UserRefreshTokens obj = new UserRefreshTokens
		{               
			RefreshToken = token.Refresh_Token,
			UserName = usersdata.Name
		};

		userServiceRepository.AddUserRefreshTokens(obj);
		userServiceRepository.SaveCommit();
		return Ok(token);
	}

	[AllowAnonymous]
	[HttpPost]
	[Route("refresh")]
	public IActionResult Refresh(Tokens token)
	{
		var principal = jWTManager.GetPrincipalFromExpiredToken(token.Access_Token);
		var username = principal.Identity?.Name;

		//retrieve the saved refresh token from database
		var savedRefreshToken = userServiceRepository.GetSavedRefreshTokens(username, token.Refresh_Token);

		if (savedRefreshToken.RefreshToken != token.Refresh_Token)
		{
			return Unauthorized("Invalid attempt!");
		}

		var newJwtToken = jWTManager.GenerateRefreshToken(username);

		if (newJwtToken == null)
		{
			return Unauthorized("Invalid attempt!");
		}

		// saving refresh token to the db
		UserRefreshTokens obj = new UserRefreshTokens
		{
			RefreshToken = newJwtToken.Refresh_Token,
			UserName = username
		};

		userServiceRepository.DeleteUserRefreshTokens(username, token.Refresh_Token);
		userServiceRepository.AddUserRefreshTokens(obj);
		userServiceRepository.SaveCommit();

		return Ok(newJwtToken);
	}
}

Here we decorated with the [Authorize] attribute at the controller level to ensure that no action methods are invoked without authentication.

Similarly, for Authenticate and Refresh IAction method, we decorated with the [AllowAnonymous] attribute, which overrides the [Authorize] attribute. This allows all users to access the Authenticate and Refresh IAction method, which returns the JWT access token and refresh token if the user is valid.

Let's consume these APIs from Postman:

Fig1: Here 1st we call authenticate API with username and password




Fig2: Here we call GET request and pass the access token, which we got after authentication.




Fig 3: Here we call the same GET API, but this time our JWT access token gets expired, and it returns is-token-expired as true in the response header.




Fig4: Here we call the POST request refresh method to re-generate the new JWT access token.



Fig 5:  Here now we call the GET method with the newly generated JWT access token from the refresh token in ASP.NET CORE



Conclusion: Here in this article we learned in detail about refresh JWT token workflow. And by using the refresh token we re-generate a new JWT access token without re-login the user again and again. You can download the complete source code from Github, and if you find this helpful so pls share this and give me a star ⭐ to encourage me more.

Other Reference:

Thank you for reading, pls keep visiting this blog and share this in your network. Also, I would love to hear your opinions down in the comments.

PS: If you found this content valuable and want to thank me? 👳 Buy Me a Coffee

Subscribe to our newsletter

Get the latest and greatest from Codepedia delivered straight to your inbox.


Post Comment

Your email address will not be published. Required fields are marked *

10 Comments