Refresh Token in ASP.NET Core Web API using JWT Authentication

Refresh Token in ASP.NET Core Web API using JWT Authentication:

In this article, I will discuss how to implement Refresh Token in ASP.NET Core Web API Application using JWT Authentication. Please read our previous article discussing JWT Authentication in ASP.NET Core Web API. We will work with the same applications we created in our previous article.

What is a Refresh Token?

A refresh token is a special kind of token that can be used to obtain a renewed access token. This token is required to continue accessing protected resources without forcing the user to authenticate again. Refresh tokens are issued alongside access tokens when users authenticate. 

Refresh tokens generally have a longer lifespan than access tokens. While an access token might expire in minutes or hours, a refresh token can last for days, weeks, or even months, depending on the application’s security requirements.

Note: Not all applications will issue refresh tokens. For instance, highly sensitive applications (such as Banking Applications) might opt not to use refresh tokens to avoid the risks associated with token theft.

How Refresh Tokens Work in ASP.NET Core Web API?

Refresh tokens are an essential part of modern authentication systems, especially in applications that use JWTs (JSON Web Tokens) for access tokens. They are used to allow users to obtain new access tokens without requiring them to log in repeatedly. For a better understanding, please have a look at the following diagram:

How Refresh Tokens Work in ASP.NET Core Web API?

The following is a detailed explanation of how refresh tokens work:

Step 1: Initial Authentication: When a user first logs in with their credentials (for example, username and password), the authentication server issues both an access token and a refresh token upon successful authentication. The access token is used to access protected resources, and the refresh token is used to obtain new access tokens after the original expires.

Step 2: Access Token Expiry: Access tokens have a short lifespan, which minimizes the risk if compromised. After the access token expires (which can happen within minutes to hours), the client can no longer use it to access protected API resources.

Step 3: Using the Refresh Token:

  • The client sends the refresh token to a specific endpoint on the authentication server, typically a refresh token.
  • The server validates the refresh token. This validation might involve checking the token’s integrity, expiry, and whether it has been revoked.

Step 4: Issuing a New Access Token: If the refresh token is valid, the server issues a new access token (and possibly a new refresh token).

Step 5: Continued Access: The client uses the new access token to access protected resources. This cycle continues until the refresh token expires or is revoked.

Implementing Refresh Token in ASP.NET Core Web API using JWT Authentication.

Implementing refresh tokens in an ASP.NET Core Web API Application using JWT Authentication involves adding functionality to issue and validate access tokens (short-lived) and refresh tokens (long-lived).

Database Setup for Refresh Tokens

We should have a mechanism to store and manage refresh tokens securely in our database. For this purpose, we will use EF Core and SQL Server databases.

Setup Entity Framework Core

First, make sure you have the necessary EF Core package for SQL Server installed: Install-Package Microsoft.EntityFrameworkCore.SqlServer

Also, install the EF Core tools for migrations: Install-Package Microsoft.EntityFrameworkCore.Tools

Creating RefreshToken Model

Next, create a class file named RefreshToken.cs within the Models folder and then copy and paste the following code:

namespace JWTAuthServer.Models
{
    public class RefreshToken
    {
        public int Id { get; set; }
        public string Token { get; set; }
        public string Username { get; set; }
        public DateTime ExpiryDate { get; set; }
    }
}
Configure the Database Connection

Instead of hard-coding the connection string with the DbContext class, we will store it in the appsettings.json file. Modify the appsettings.json file as follows. Here, we have added the connecting string and the JWT keys.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "KHPK6Ucf/zjvU4qW8/vkuuGLHeIo0l9ACJiTaAPLKbk=", //Secret Key
    "Issuer": "https://localhost:7035", //Authentication Server Domain URL
    "Audience": "http://localhost:5001" //Client Application Domain URL
  },
  "ConnectionStrings": {
    "EFCoreAuthDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=AuthDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Creating the DbContext

Next, create a DbContext that includes a DbSet for the RefreshToken entities. We need to create a class that inherits from DbContext. So, create a class file named AuthDbContext within the Models folder and copy and paste the following code.

using Microsoft.EntityFrameworkCore;
namespace JWTAuthServer.Models
{
    public class AuthDbContext : DbContext
    {
        public AuthDbContext(DbContextOptions<AuthDbContext> options) : base(options)
        {
        }

        public DbSet<RefreshToken> RefreshTokens { get; set; }
    }
}
Registering the Connection String and DbContext Class:

Next, we need to configure the connection string and register the Db Context class to use the Connection string in the Program class. So, modify the Program class as follows.

using JWTAuthServer.Models;
using Microsoft.EntityFrameworkCore;

namespace JWTAuthServer
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the DI container. This includes controllers, Swagger for API documentation, and endpoints.
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            //Configure the ConnectionString and DbContext class
            builder.Services.AddDbContext<AuthDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreAuthDBConnection"));
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            // Registers the authentication middleware that uses the default authentication scheme set by AddAuthentication.
            app.UseAuthentication();

            // Registers the authorization middleware to enforce authorization policies on the app's routes.
            app.UseAuthorization();

            // Maps controller routes.
            app.MapControllers();

            app.Run();
        }
    }
}
Database Migration

Next, we need to generate the database Migration script and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows. You can give your migration any name. Here, I am giving it Mig1. The name that you are giving it should not be given earlier.

how to implement Refresh Token in ASP.NET Core Web API Application using JWT Authentication

With this, our AuthDB Database with RefreshTokens table is created, as shown in the below image:

Refresh Token in ASP.NET Core Web API Application using JWT Authentication

Implement Data Access Methods for Managing Refresh Tokens

Next, we need to implement the methods in a service class to perform the database CRUD Operations. So, create a class file named TokenService.cs within the Models folder and copy and paste the following code. The following class is used for managing refresh tokens within a JWT authentication system:

using Microsoft.EntityFrameworkCore;

namespace JWTAuthServer.Models
{
    // Defines a service class for managing refresh tokens.
    public class TokenService
    {
        // Dependency on the database context used to interact with the database.
        private readonly AuthDbContext _context;

        // Constructor that initializes the TokenService with an instance of the database context.
        public TokenService(AuthDbContext context)
        {
            _context = context;  
        }

        // Asynchronously saves a new refresh token to the database.
        public async Task SaveRefreshToken(string username, string token)
        {
            // Create a new RefreshToken object.
            var refreshToken = new RefreshToken
            {
                Username = username,  // Set the username associated with the token.
                Token = token,  // Set the token value.
                ExpiryDate = DateTime.UtcNow.AddDays(7)  // Set the expiration date to 7 days from the current UTC date/time.
            };

            // Add the new refresh token to the corresponding DbSet in the database context.
            _context.RefreshTokens.Add(refreshToken);
            // Asynchronously save changes to the database, which includes inserting the new refresh token.
            await _context.SaveChangesAsync();
        }

        // Asynchronously retrieves the username associated with a specific refresh token.
        public async Task<string> RetrieveUsernameByRefreshToken(string refreshToken)
        {
            // Asynchronously find a refresh token that matches the provided token and has not yet expired.
            var tokenRecord = await _context.RefreshTokens
                .FirstOrDefaultAsync(rt => rt.Token == refreshToken && rt.ExpiryDate > DateTime.UtcNow);

            // Return the username if the token is found and valid, otherwise null.
            return tokenRecord?.Username;
        }

        // Asynchronously revokes (deletes) a refresh token from the database.
        public async Task<bool> RevokeRefreshToken(string refreshToken)
        {
            // Asynchronously find the refresh token in the database.
            var tokenRecord = await _context.RefreshTokens
                .FirstOrDefaultAsync(rt => rt.Token == refreshToken);

            // If the token is found, remove it from the DbSet.
            if (tokenRecord != null)
            {
                _context.RefreshTokens.Remove(tokenRecord);
                // Save changes to the database asynchronously to reflect the token removal.
                await _context.SaveChangesAsync();
                return true;  // Return true to indicate successful revocation.
            }

            // Return false if no matching token was found, indicating no revocation was performed.
            return false;
        }
    }
}

The above Token Service class provides a clear implementation for managing refresh tokens, including saving, retrieving, and revoking tokens. Each method handles its task with respect to the database, ensuring tokens are managed securely and efficiently.

Register the TokenService in the Program Class:

Next, register the TokenService in the Program.cs class file to make it available throughout our application via dependency injection. So, please add the following line of code to the Program class:

// Register the TokenService
builder.Services.AddScoped<TokenService>();
Creating RefreshTokenRequest Model:

Next, add a class file named RefreshTokenRequest.cs within the Models folder and copy and paste the following code. This model holds a single property that is required when we invoke the Refresh Token and Revoke Refresh Token endpoints.

using System.ComponentModel.DataAnnotations;

namespace JWTAuthServer.Models
{
    public class RefreshTokenRequest
    {
        [Required]
        public string RefreshToken {  get; set; }
    }
}
Modifying AuthController:

Next, we need to implement the Refresh token in our Auth Controller. So, please modify the AuthController as follows. The following AuthController manages authentication processes, including login, refresh token, and revocation of refresh token. The following code self-explains; please read the comment lines for a better understanding.

using JWTAuthServer.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWTAuthServer.Controllers
{
[Route("api/[controller]")]  
[ApiController]  
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration;  // Configuration object to access application settings.
private readonly TokenService _tokenService;  // Service for managing refresh tokens.
// Constructor with dependency injection for configuration and the token service.
public AuthController(IConfiguration configuration, TokenService tokenService)
{
_configuration = configuration;  // Initialize configuration.
_tokenService = tokenService;  // Initialize token service.
}
// Action method to handle login requests. It expects a Login model in the request body.
[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] Login request)
{
// Validate the model state (input validation).
if (!ModelState.IsValid)
{
return BadRequest("Invalid request body.");  // Return bad request if the model state is invalid.
}
try
{
// Attempt to find a user that matches the provided username and password.
var user = UserStore.Users.FirstOrDefault(u => u.Username == request.Username && u.Password == request.Password);
// Check if user was not found.
if (user == null)
{
return Unauthorized("Invalid user credentials.");  // Return unauthorized if no user is found.
}
// Issue a new access token for the user.
var accessToken = IssueAccessToken(user);
// Generate a new refresh token.
var refreshToken = GenerateRefreshToken();
// Save the new refresh token with associated user information.
await _tokenService.SaveRefreshToken(user.Username, refreshToken);
// Return the generated access and refresh tokens.
return Ok(new { AccessToken = accessToken, RefreshToken = refreshToken });
}
catch (Exception ex)
{
// Handle any exceptions that occur during the login process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
// Generates an access token for the specified user.
private string IssueAccessToken(User user)
{
// Create a new symmetric security key from the configured JWT key.
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
// Setup the signing credentials to use for the token.
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// Prepare claims to be included in the token.
var claims = new List<Claim>
{
new Claim("Myapp_User_Id", user.Id.ToString()),  // Custom claim for user ID.
new Claim(ClaimTypes.NameIdentifier, user.Username),  // Standard claim for user identifier.
new Claim(ClaimTypes.Email, user.Email),  // Standard claim for user's email.
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString())  // JWT standard claim for subject.
};
// Add role claims for the user.
user.Roles.ForEach(role => claims.Add(new Claim(ClaimTypes.Role, role)));
// Create the JWT token using the claims, issuer, audience, etc., and set its expiration.
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(30),  // Set access token to expire in 30 minutes.
signingCredentials: credentials);
// Return the serialized token.
return new JwtSecurityTokenHandler().WriteToken(token);
}
// Generates a new refresh token using a cryptographic random number generator.
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];  // Prepare a buffer to hold the random bytes.
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);  // Fill the buffer with cryptographically strong random bytes.
return Convert.ToBase64String(randomNumber);  // Convert the bytes to a Base64 string and return.
}
}
// Refreshes an access token using a valid refresh token.
[HttpPost("RefreshToken")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
// Validate the refresh token request.
if (request == null || string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest("Refresh token is required.");  // Return bad request if no refresh token is provided.
}
try
{
// Retrieve the username associated with the provided refresh token.
var username = await _tokenService.RetrieveUsernameByRefreshToken(request.RefreshToken);
if (string.IsNullOrEmpty(username))
{
return Unauthorized("Invalid refresh token.");  // Return unauthorized if no username is found (invalid or expired token).
}
// Retrieve the user by username.
var user = UserStore.Users.FirstOrDefault(u => u.Username == username);
if (user == null)
{
return Unauthorized("Invalid user.");  // Return unauthorized if no user is found.
}
// Issue a new access token and refresh token for the user.
var accessToken = IssueAccessToken(user);
var newRefreshToken = GenerateRefreshToken();
// Save the new refresh token.
await _tokenService.SaveRefreshToken(user.Username, newRefreshToken);
// Return the new access and refresh tokens.
return Ok(new { AccessToken = accessToken, RefreshToken = newRefreshToken });
}
catch (Exception ex)
{
// Handle any exceptions during the refresh process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
// Revokes a refresh token to prevent its future use.
[HttpPost("RevokeToken")]
public async Task<IActionResult> RevokeToken([FromBody] RefreshTokenRequest request)
{
// Validate the revocation request.
if (request == null || string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest("Refresh token is required.");  // Return bad request if no refresh token is provided.
}
try
{
// Attempt to revoke the refresh token.
var result = await _tokenService.RevokeRefreshToken(request.RefreshToken);
if (!result)
{
return NotFound("Refresh token not found.");  // Return not found if the token does not exist.
}
return Ok("Token revoked.");  // Return success message if the token is successfully revoked.
}
catch (Exception ex)
{
// Handle any exceptions during the revocation process.
return StatusCode(500, $"Internal server error: {ex.Message}");  // Return a 500 internal server error on exception.
}
}
}
}
Understanding Login Endpoint ([HttpPost(“Login”)])

Authenticate users based on their provided username and password. If successful, issue both an access token and a refresh token.

  • Access Token: A short-lived token that grants the user access to protected resources.
  • Refresh Token: A longer-lived token used to request new access tokens once the current access token expires without requiring the user to re-authenticate.
Understanding IssueAccessToken Method

Generate a JSON Web Token (JWT) that serves as the access token. This method is called after successful authentication or when refreshing an access token. The token includes user-specific claims such as user ID, username, and email, along with any roles assigned to the user. The token is signed and has a limited lifespan.

Understanding GenerateRefreshToken Method

Create a new, cryptographically secure refresh token. This method is used during the login process and when refreshing the access token. It generates a random string that is used as the refresh token, ensuring that it is hard to guess.

Understanding RefreshToken Endpoint ([HttpPost(“RefreshToken”)])

Allow users to obtain a new access token using a valid refresh token when their current access token expires. Checks if the refresh token is provided and valid. If valid, it fetches the user details associated with the refresh token. Issues a new access token and a new refresh token, replacing the old refresh token to maintain security through token rotation.

Understanding RevokeToken Endpoint ([HttpPost(“RevokeToken”)])

Provide a mechanism to revoke (invalidate) a refresh token when it is no longer needed or if it should be prematurely terminated (e.g., user logout, security concerns). Ensures a refresh token is provided in the request. Attempts to find and remove the refresh token from the store, effectively preventing any future use of that token to obtain new access tokens.

Testing the Functionalities:

Now, let us test the Login, Refresh Token, and Revoke Token Endpoints. We will test the functionalities using Postman.

Testing Login Endpoint:

When we make the Request to the Login endpoint, we will receive the Access Token and Refresh Token if the provided user information is valid. For a better understanding, please check the following:

Refresh Token in ASP.NET Core Web API using JWT Authentication

Now, if you verify the database, then you will see the above Refresh Token, user name, and the Refresh Token expiration time as shown in the below image:

Refresh Token in ASP.NET Core Web API using JWT Authentication

Testing Refresh Token Endpoint:

When we make a Request to the Refresh Token End Point, we will receive the Access Token and possibly a new Refresh Token if the provided Refresh Token is valid. For a better understanding, please check the following:

Testing Refresh Token Endpoint

Testing Revoke Refresh Token Endpoint:

When we make a Request to the Revoke Refresh Token Endpoint, it will delete the Refresh Token from the database if the provided Refresh Token is valid. For a better understanding, please check the following:

Testing Revoke Refresh Token Endpoint

Note: When implementing refresh token rotation, it is generally recommended to invalidate or delete the old refresh token once a new one has been issued. This helps ensure that each refresh token can only be used once, enhancing the system’s security by preventing the reuse of tokens.

Note: No need to make any changes in the Resource Server Application.

Modifying the Client Application:

Next, we need to modify the Client Application, which consumes the Resource Server APIs with JWT Authentication. If the Access Token has expired, we need to get a new one using the Refresh Token. So, modify the Console Application Program class as follows. The following code is self-explained. For a better understanding, please read the comment lines.

using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace JWTClientApp
{
public class Program
{
// Static HttpClient to be reused throughout the lifetime of the application.
private static readonly HttpClient httpClient = new HttpClient();
// Base URL for user-related API operations.
private static readonly string baseUrl = "https://localhost:7239/api/users";
// Main method, the entry point of the program.
static async Task Main(string[] args)
{
try
{
// Authenticate and get both access and refresh tokens.
var tokens = await AuthenticateAndGetToken();
Console.WriteLine("Access Token received: " + tokens.AccessToken);
Console.WriteLine("Refresh Token received: " + tokens.RefreshToken);
// Example CRUD operations using the received tokens.
Console.WriteLine(await GetUser(tokens.AccessToken, tokens.RefreshToken, 1)); // Get user with ID 1.
Console.WriteLine(await CreateUser(tokens.AccessToken, tokens.RefreshToken, new User { Id = 4, Username = "newuser", Email = "newuser@example.com" }));
Console.WriteLine(await UpdateUser(tokens.AccessToken, tokens.RefreshToken, 4, new User { Id = 4, Username = "updatedUser", Email = "updateduser@example.com" }));
Console.WriteLine(await DeleteUser(tokens.AccessToken, tokens.RefreshToken, 4)); // Delete user with ID 4.
Console.ReadKey(); // Pause the console so the user can read the output.
}
catch (Exception ex)
{
// Catch any exceptions and display the error message.
Console.WriteLine("Error: " + ex.Message);
}
}
// Authenticates with the API to retrieve access and refresh tokens.
static async Task<(string AccessToken, string RefreshToken)> AuthenticateAndGetToken()
{
var loginUrl = "https://localhost:7035/api/Auth/Login"; // URL for the login endpoint.
var loginRequestBody = new { Username = "admin", Password = "password" }; // Request body with credentials.
var requestContent = new StringContent(JsonSerializer.Serialize(loginRequestBody), Encoding.UTF8, "application/json"); // Serialize the body to JSON.
var response = await httpClient.PostAsync(loginUrl, requestContent); // Send the login request.
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(); // Read the error message from the response.
throw new Exception($"Login failed: {errorContent}"); // Throw an exception if login fails.
}
var loginResponseContent = await response.Content.ReadAsStringAsync(); // Read the successful login response.
var tokenObject = JsonSerializer.Deserialize<JsonElement>(loginResponseContent); // Deserialize the JSON response to access the tokens.
var accessToken = tokenObject.GetProperty("AccessToken").GetString(); // Extract the access token.
var refreshToken = tokenObject.GetProperty("RefreshToken").GetString(); // Extract the refresh token.
return (AccessToken: accessToken, RefreshToken: refreshToken); // Return both tokens.
}
// Refreshes the access token using a refresh token.
static async Task<(string AccessToken, string RefreshToken)> RefreshAccessToken(string refreshToken)
{
var refreshUrl = "https://localhost:7035/api/Auth/RefreshToken"; // URL for the refresh token endpoint.
var refreshContent = new StringContent(JsonSerializer.Serialize(new { RefreshToken = refreshToken }), Encoding.UTF8, "application/json"); // Serialize the refresh token request to JSON.
var response = await httpClient.PostAsync(refreshUrl, refreshContent); // Send the refresh token request.
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(); // Read the error message from the response.
throw new Exception($"Refresh token failed: {errorContent}"); // Throw an exception if refresh fails.
}
var refreshResponseContent = await response.Content.ReadAsStringAsync(); // Read the successful refresh response.
var tokenObject = JsonSerializer.Deserialize<JsonElement>(refreshResponseContent); // Deserialize the JSON response to access the new tokens.
var accessToken = tokenObject.GetProperty("AccessToken").GetString(); // Extract the new access token.
var newRefreshToken = tokenObject.GetProperty("RefreshToken").GetString(); // Extract the new refresh token.
return (AccessToken: accessToken, RefreshToken: newRefreshToken); // Return the refreshed tokens.
}
static async Task<string> GetUser(string accessToken, string refreshToken, int userId)
{
var response = await SendAuthorizedRequest(HttpMethod.Get, $"{baseUrl}/{userId}", accessToken, null);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var tokens = await RefreshAccessToken(refreshToken);
//Update the Refresh and Access Token
refreshToken = tokens.RefreshToken;
accessToken = tokens.AccessToken;
response = await SendAuthorizedRequest(HttpMethod.Get, $"{baseUrl}/{userId}", accessToken, null);
}
return await ProcessResponse(response, "Get User");
}
static async Task<string> CreateUser(string accessToken, string refreshToken, User user)
{
//var response = await SendAuthorizedRequest(HttpMethod.Post, baseUrl, accessToken, new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json"));
//if (response.StatusCode == HttpStatusCode.Unauthorized)
//{
//    var tokens = await RefreshAccessToken(refreshToken);
//    //Update the Refresh and Access Token
//    refreshToken = tokens.RefreshToken;
//    accessToken = tokens.AccessToken;
//    response = await SendAuthorizedRequest(HttpMethod.Post, baseUrl, accessToken, new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json"));
//}
//Testing the Refresh Token Manually
var tokens = await RefreshAccessToken(refreshToken);
refreshToken = tokens.RefreshToken;
accessToken = tokens.AccessToken;
var response = await SendAuthorizedRequest(HttpMethod.Post, baseUrl, accessToken, new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json"));
return await ProcessResponse(response, "Create User");
}
static async Task<string> UpdateUser(string accessToken, string refreshToken, int userId, User user)
{
var response = await SendAuthorizedRequest(HttpMethod.Put, $"{baseUrl}/{userId}", accessToken, new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json"));
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var tokens = await RefreshAccessToken(refreshToken);
//Update the Refresh and Access Token
refreshToken = tokens.RefreshToken;
accessToken = tokens.AccessToken;
response = await SendAuthorizedRequest(HttpMethod.Put, $"{baseUrl}/{userId}", accessToken, new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json"));
}
return await ProcessResponse(response, "Update User");
}
static async Task<string> DeleteUser(string accessToken, string refreshToken, int userId)
{
var response = await SendAuthorizedRequest(HttpMethod.Delete, $"{baseUrl}/{userId}", accessToken, null);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var tokens = await RefreshAccessToken(refreshToken);
//Update the Refresh and Access Token
refreshToken = tokens.RefreshToken;
accessToken = tokens.AccessToken;
response = await SendAuthorizedRequest(HttpMethod.Delete, $"{baseUrl}/{userId}", accessToken, null);
}
return await ProcessResponse(response, "Delete User");
}
// Generic method to send authorized HTTP requests and handle automatic token refresh if needed.
static async Task<HttpResponseMessage> SendAuthorizedRequest(HttpMethod method, string url, string token, HttpContent content)
{
var request = new HttpRequestMessage(method, url) // Prepare the HTTP request.
{
Content = content, // Set the request content if any.
Headers =
{
Authorization = new AuthenticationHeaderValue("Bearer", token) // Set the authorization header using the bearer token.
}
};
return await httpClient.SendAsync(request); // Send the request.
}
// Process the HTTP response, return a formatted string indicating success or type of failure.
static async Task<string> ProcessResponse(HttpResponseMessage response, string action)
{
var responseBody = await response.Content.ReadAsStringAsync(); // Read the response body as a string.
if (response.IsSuccessStatusCode)
{
return $"{action} succeeded: {responseBody}"; // Return success message with the response body.
}
switch (response.StatusCode)
{
case HttpStatusCode.Unauthorized:
return $"{action} Failed: Unauthorized - Token may be invalid or expired"; // Handle unauthorized responses.
case HttpStatusCode.Forbidden:
return $"{action} Failed: Forbidden - Insufficient permissions"; // Handle forbidden responses.
default:
return $"{action} Failed: {response.StatusCode} - {responseBody}"; // Handle other types of errors.
}
}
}
// Simple user model to demonstrate API data interactions.
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
}
Output:

Why Not Long-Lived Access Token? Why Refresh Token in Web API?

Why Not Long-Lived Access Token? Why Refresh Token in Web API?

Now, you may have one question in your mind. Why are we issuing a long-lived access token for the first time? Let’s discuss why there is no long-lived access token or the advantages of using a refresh token in ASP.NET Core Web API when using JWT JWT-based authentication. Mainly there are three main reasons to use the refresh tokens are as follows:

Updating the Access Token Content:

As we already discussed, access tokens are self-contained, meaning they contain all the information (known as claims) of an authenticated user once they are generated.

Now, if we issue a long-lived access token, let’s say, for example, 1 month, for a user, let’s say Anurag, and let’s say Anurag is enrolled with the role Users at the moment, all this information gets stored on the access token, which is generated by the Authorization server.

If you have decided (3 days after he obtained the access token) to add him with the role Admin. Then, there is no way to update this information in the access token that has already been generated. You need to ask him to re-authenticate himself again so that the Authorization server can add the updated information to the newly generated access token. This is not feasible in most of the cases. You might not be able to reach the users who have already obtained the long-lived access tokens.

So, to overcome the above issue, we need to issue a short-lived access token (30 minutes, for example) along with a long-lived refresh token. The user needs to use the refresh token to obtain the newly updated access token. Once the user obtains the new access token, the Authorization Server will be able to add the updated claims or new claims to the new access token being generated.

Revoking the Access from Authenticated Users:

Once the user obtains the long-lived access token, he will be able to access the server resources as long as his access token has not expired. There is no standard way to revoke access tokens unless and until the Authorization Server implements some custom logic to store the generated access token in a database and needs to do database checks with each request.

However, with the refresh token, a database or system admin can simply revoke access by deleting the refresh token identifier from the database. So, when the user requests a new access token using the deleted refresh token, the Authorization Server will reject this request because the refresh token is no longer available in the database.

No need to store or ask for the username and password frequently:

Using a refresh token allows you to ask the user for his username and password only one time (i.e., for the first time), then the Authorization Server can issue a very long-lived refresh token (1 year, for example), and the user will stay logged in all this period until and unless system admin tries to revoke (delete) the refresh token. This can be very useful if you are building an API that will be consumed by a front-end application where it is not feasible to keep asking for the username/password frequently.

So, for the above three major reasons, we need to use Refresh Tokens. In Web Applications, refresh tokens enhance the user experience by reducing the number of times users must actively re-authenticate, thereby seamlessly maintaining session continuity. This is useful in mobile apps and Single-Page Applications (SPAs) where frequent user interaction for re-authentication can disrupt the user experience.

In the next article, I will discuss how to implement Role-Based JWT Authentication in ASP.NET Core Web API Application. In this article, I explain how to Implement a Refresh Token in JWT Token-Based Authentication in an ASP.NET Core Web API Application with an Example. I hope you enjoy this article on implementing a Refresh Token in JWT Token-Based Authentication in an ASP.NET Core Web API application.

Leave a Reply

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