JWT Refresh Token
This article will go through an example of how to implement JWT (JSON Web Token) authentication with refresh tokens in an ASP.NET Core 5.0 API. Before reading this blog there are some prerequisites as below :
- .NET Core API
- ASP.NET 5.0
- Must know about JWT Authentication
- Postman Tool
- Visual Studio 2022 (an earlier version will also work but here I’m using Visual Studio 2022)
Before we go into the implementation of the JWT refresh token let’s have a quick look at the JWT Authentication.
What is JWT (JSON Web Token)?
JSON web tokens provide a safe way to send data in the form of a JSON object between two parties. It’s an open standard and a widely used web authentication method to securely send a user’s data between the clients and servers. JSON Web Tokens can be used in.NET, Python, Node.js, Java, PHP, Ruby, Go, JavaScript, and other programming languages.
A login form is available in one application. A login button is pressed after the user has entered their username and password. A client (e.g., a web browser) submits the user’s information to the server’s API endpoint after pressing the login button. The server will deliver an encoded JWT to the client once it has verified the user’s credentials and confirmed that the user is valid. A JSON web token is a JavaScript object that can hold the logged-in user’s information. It may include a username, a user’s subject, user roles, or other pertinent information.
Need for Refresh Token
If we utilize an access token for a long time, a hacker may steal it and misuse it. As a result, using the access token for an extended period is not recommended. Typically, refresh tokens are used to obtain new access tokens. We can utilize refresh tokens to get a new access token from the authentication controller when the access tokens expire. A refresh token normally has a significantly longer lifespan than an access token. An access token will have a short lifetime. As a result, even a hacker’s access token is only valid for a short time.
- To begin, the client provides credentials to the authentication component.
- The access token and refresh token are then issued by the authentication component.
- After then, the client uses the access token to request resource endpoints for a protected resource.
- The resource endpoint verifies the access token before delivering a secure resource.
- Repeat steps 3 and 4 until the access token is no longer valid.
- When the access token expires, the client requests a new one using the refresh token.
- A new access token and refresh token are issued by the authentication component.
- Repeat steps 3 through 7 until the refresh token has expired.
- When the refresh token expires, the client must re-authenticate with the authentication server, and the flow begins again at step 1.
Implementation
Here first we create the Asp.net Core Web API project. I am using Asp.net Core 5 version. After Creating project install following NuGet packages using Visual Studio Nuget Package Manager UI
Tools → NuGet Package Manager → Manage NuGet Packages for Solution.
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- System.IdentityModel.Tokens.Jwt
Project structure should looks like image given above.
Note : No need to add Migration folder in project manually, it will automatically create when in add migration and database into MS SQL Server.
Add the following code to the appsettings.json file.
"ConnectionStrings":
{
"SqlServerDbCon": "Server=DESKTOP-VQAK5AI\\SQLEXPRESS;Database=RefreshTokenDB;Trusted_Connection=True;"
},
"JWT":
{
"Key": "This is my secret key for jwt refresh token",
"Issuer": "https://localhost:5000",
"Audience": "https://localhost:3000"
},
With Connection string we need to set path of database connection which you are using. Here I am using MS SQL Server 2018 Database. Add JWT Secret key, Issuer and Audience. These values are needed when the JWT token will generate.
Create and add the following models to Model Folder
UserRegister.cs
using System.ComponentModel.DataAnnotations;
namespace RefreshTokenDemo.Models
{
public class UserRegister
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
}
UserLogin.cs
namespace RefreshTokenDemo.Models
{
public class UserLogin
{
public string Email { get; set; }
public string Password { get; set; }
}
}
UserRefreshTokens.cs
using System.ComponentModel.DataAnnotations;
namespace RefreshTokenDemo.Models
{
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;
}
}
Tokens.cs
namespace RefreshTokenDemo.Models
{
public class Tokens
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}
AppDbContext.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace RefreshTokenDemo.Models
{
public class AppDbContext : IdentityDbContext<IdentityUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public virtual DbSet<UserRefreshTokens> UserRefreshToken { get; set; }
public virtual DbSet<UserRegister> UserRegisters { get; set; }
}
}
Add IJWTManagerRepository interface in Repository folder and write following code :
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.
Implement interface “IJWTManagerRepository.cs”. To implement interface add class “JWTManagerRepository.cs”.
public class JWTManagerRepository : IJWTManagerRepository
{
private readonly IConfiguration _iconfiguration;
public JWTManagerRepository(IConfiguration iconfiguration)
{
_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 { AccessToken = tokenHandler.WriteToken(token), RefreshToken = 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;
}
}
Create new interface “IUserServiceRepository.cs” in Repository folder and add the following code:
public interface IUserServiceRepository
{
Task<bool> IsValidUserAsync(UserLogin users);
UserRefreshTokens AddUserRefreshTokens(UserRefreshTokens user);
UserRefreshTokens GetSavedRefreshTokens(string username, string refreshtoken);
void DeleteUserRefreshTokens(string username, string refreshToken);
}
Implement interface “IUserServiceRepository.cs”. To implement interface add class “UserServiceRepository.cs”.
public class UserServiceRepository : IUserServiceRepository
{
private readonly AppDbContext _db;
public UserServiceRepository(UserManager<IdentityUser> userManager, AppDbContext db)
{
this._db = db;
}
public UserRefreshTokens AddUserRefreshTokens(UserRefreshTokens user)
{
_db.UserRefreshToken.Add(user);
_db.SaveChanges();
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 async Task<bool> IsValidUserAsync(UserLogin users)
{
var u = _db.UserRegisters.FirstOrDefault(o => o.Email == users.Email && o.Password==users.Password);
if (u != null)
return true;
else
return false;
}
}
Write the following code into “Startup.cs“ file.
In startup.cs file under ConfigureServices method first we configure database connectivity using services.AddDbContext(). Next, we add services.AddIdentity() which configures the Identity system for users and roles types.
And last but not least we add services.AddAuthentication() and AddJwtBearer(), which enables JWT Bearer Authentication in our Asp.net Core application. In Configure() method “addapp.UseAuthentication()” this need to be added before UseAuthorization()
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using RefreshTokenDemo.Models;
using RefreshTokenDemo.Repository;
using System;
using System.Text;
namespace RefreshTokenDemo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SqlServerDbCon")));
services.AddIdentity<IdentityUser, IdentityRole>(options => {
options.Password.RequireUppercase = true;
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,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Key),
ClockSkew = TimeSpan.Zero
};
});
services.AddSingleton<IJWTManagerRepository, JWTManagerRepository>();
services.AddScoped<IUserServiceRepository, UserServiceRepository>();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "RefreshTokenDemo", Version = "v1" });
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RefreshTokenDemo v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); // add before UseAuthorization()
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Create “UsersController” controller in Controller folder.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using RefreshTokenDemo.Models;
using RefreshTokenDemo.Repository;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace RefreshTokenDemo.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IJWTManagerRepository _jWTManager;
private readonly IUserServiceRepository _userServiceRepository;
public UsersController(IJWTManagerRepository jWTManager, IUserServiceRepository userServiceRepository)
{
_jWTManager = jWTManager;
_userServiceRepository = userServiceRepository;
}
[HttpGet]
public List<string> Get()
{
var usersList = new List<string>
{
"Shubham Chauhan",
"Kunal Parmar",
"Dipak Kushwaha"
};
return usersList;
}
[AllowAnonymous]
[HttpPost]
[Route("authenticate-user")]
public async Task<IActionResult> AuthenticateAsync(UserLogin usersdata)
{
var validUser = await _userServiceRepository.IsValidUserAsync(usersdata);
if (!validUser)
{
return Unauthorized("Invalid username or password...");
}
var token = _jWTManager.GenerateToken(usersdata.Email);
if (token == null)
{
return Unauthorized("Invalid Attempt..");
}
UserRefreshTokens obj = new UserRefreshTokens
{
RefreshToken = token.RefreshToken,
UserName = usersdata.Email
};
_userServiceRepository.AddUserRefreshTokens(obj);
return Ok(token);
}
[AllowAnonymous]
[HttpPost]
[Route("refresh-token")]
public IActionResult Refresh(Tokens token)
{
var principal = _jWTManager.GetPrincipalFromExpiredToken(token.AccessToken);
var username = principal.Identity?.Name;
var savedRefreshToken = _userServiceRepository.GetSavedRefreshTokens(username, token.RefreshToken);
if (savedRefreshToken.RefreshToken != token.RefreshToken)
{
return Unauthorized("Invalid attempt!");
}
var newJwtToken = _jWTManager.GenerateRefreshToken(username);
if (newJwtToken == null)
{
return Unauthorized("Invalid attempt!");
}
UserRefreshTokens obj = new UserRefreshTokens
{
RefreshToken = newJwtToken.RefreshToken,
UserName = username
};
_userServiceRepository.DeleteUserRefreshTokens(username, token.RefreshToken);
_userServiceRepository.AddUserRefreshTokens(obj);
return Ok(newJwtToken);
}
}
}
Now let’s run and check the API from POSTMAN.
Thank you for taking the time to read this blog. I greatly appreciate your support and encourage you to continue visiting and sharing it with others in your network. I value your opinions and would love to hear your thoughts in the comments section below.
Furthermore, I welcome any suggestions or feedback you may have regarding this blog. Your insights will help me improve and deliver content that resonates with you and the wider audience. Your input is invaluable, and I look forward to reading your comments.
Once again, thank you for your continued support, and I hope to see you again soon on this blog.