Implementing JWT Refresh Tokens in .NET 8.0

Introduction

In the world of web development, security is a top priority. JSON Web Tokens (JWTs) have become a popular choice to securely transmit information between parties. However, it is important to maintain user sessions without requiring frequent logins once JWTs expire. This is where JWT refresh tokens come in.

In this article, we will guide you through implementing JWT refresh tokens in a .NET 8.0 environment. We will cover each step with a complete example to ensure a clear understanding of the process.

JWT and Refresh Tokens

JSON Web Tokens (JWTs) comprise three segments: a header, a payload, and a signature. They are digitally signed to verify their authenticity and contain claims, which are statements about an entity (user) and additional data. JWTs have an expiration time (exp), after which they are considered invalid. Refresh tokens are used to obtain new JWTs once the original token expires without requiring the user to re-enter their credentials.

Setting Up the .NET Project

Let's create a simple authentication system using JWT and refresh tokens.

Firstly, ensure you have the necessary packages installed (System.IdentityModel.Tokens.Jwt and Microsoft.AspNetCore.Authentication.JwtBearer).

Models

Create two models to represent the login request and the response containing tokens.

// LoginModel.cs
public class LoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

// TokenResponse.cs
public class TokenResponse
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

Auth Controller

Create an AuthController responsible for handling authentication and token generation.

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private readonly IConfiguration _config;
    private readonly IUserService _userService;

    public AuthController(IConfiguration config, IUserService userService)
    {
        _config = config;
        _userService = userService;
    }

    [HttpPost("login")]
    public IActionResult Login(LoginModel loginModel)
    {
        // Authenticate user
        var user = _userService.Authenticate(loginModel.Username, loginModel.Password);

        if (user == null)
            return Unauthorized();

        // Generate tokens
        var accessToken = TokenUtils.GenerateAccessToken(user, _config["Jwt:Secret"]);
        var refreshToken = TokenUtils.GenerateRefreshToken();

        // Save refresh token (for demo purposes, this might be stored securely in a database)
        // _userService.SaveRefreshToken(user.Id, refreshToken);

        var response = new TokenResponse
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken
        };

        return Ok(response);
    }

    [HttpPost("refresh")]
    public IActionResult Refresh(TokenResponse tokenResponse)
    {
        // For simplicity, assume the refresh token is valid and stored securely
        // var storedRefreshToken = _userService.GetRefreshToken(userId);

        // Verify refresh token (validate against the stored token)
        // if (storedRefreshToken != tokenResponse.RefreshToken)
        //    return Unauthorized();

        // For demonstration, let's just generate a new access token
        var newAccessToken = TokenUtils.GenerateAccessTokenFromRefreshToken(tokenResponse.RefreshToken, _config["Jwt:Secret"]);

        var response = new TokenResponse
        {
            AccessToken = newAccessToken,
            RefreshToken = tokenResponse.RefreshToken // Return the same refresh token
        };

        return Ok(response);
    }
}

Token Utility

Create a utility class, TokenUtils to handle token generation and verification.

public static class TokenUtils
{
    public static string GenerateAccessToken(User user, string secret)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(secret);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddMinutes(15), // Token expiration time
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

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

    public static string GenerateAccessTokenFromRefreshToken(string refreshToken, string secret)
    {
        // Implement logic to generate a new access token from the refresh token
        // Verify the refresh token and extract necessary information (e.g., user ID)
        // Then generate a new access token

        // For demonstration purposes, return a new token with an extended expiry
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(secret);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Expires = DateTime.UtcNow.AddMinutes(15), // Extend expiration time
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}

UserService (Sample)

For demonstration purposes, here's a simple UserService that provides user authentication.

public interface IUserService
{
    User Authenticate(string username, string password);
    // void SaveRefreshToken(int userId, string refreshToken);
    // string GetRefreshToken(int userId);
}

public class UserService : IUserService
{
    private readonly List<User> _users = new List<User>
    {
        new User { Id = 1, Username = "user1", Password = "password1" }
    };

    public User Authenticate(string username, string password)
    {
        var user = _users.SingleOrDefault(x => x.Username == username && x.Password == password);
        return user;
    }

    // For demo purposes - methods to save and retrieve refresh tokens
}

User Model (Sample)

Create a simple User model.

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

Startup Configuration

In your Startup.cs, configure JWT authentication.

public void ConfigureServices(IServiceCollection services)
{
    // ...

    var secret = Configuration["Jwt:Secret"];
    var key = Encoding.ASCII.GetBytes(secret);

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key)
            };
        });

    // ...
}

Important Notes

  • This is a basic implementation for demonstration purposes. In a production environment, you should enhance security, handle token storage securely, and implement proper validation and error handling.
  • For better security, consider using libraries IdentityServer4 or other well-established authentication solutions.

Conclusion

Implementing JWT refresh tokens in .NET 8.0 involves configuring authentication middleware, generating tokens upon authentication, and refreshing expired tokens as needed. This process enhances security by allowing seamless token renewal without requiring users to log in repeatedly.

Remember to securely handle token storage and implement proper validation logic to ensure the safety and integrity of your authentication system.