.NET Core  

Implementing JWT Authentication with Redis Cache in ASP.NET Core Web API

Introduction

  • JWT (JSON Web Token) authentication is a widely used approach to secure Web APIs. However, validating tokens on every request can be performance-intensive, especially when handling thousands of concurrent users. To overcome this bottleneck, we can cache token data in an in-memory store like Redis, which offers fast access, scalability, and built-in expiry support.
  • In this article, we’ll walk through how to implement JWT-based authentication in an ASP.NET Core Web API application and enhance it using Redis for token storage.

What You'll Build

  • A secure login endpoint that issues JWT and refresh tokens.
  • Redis-based token storage for quick validation.
  • A clean and scalable architecture with separation of concerns.

Project Structure

Component Responsibility
AuthController Handles login and token generation
ITokenService Interface for token generation logic
RedisManager Handles Redis token storage/retrieval
TokenModel Represents token metadata
Program.cs Configures DI, Redis, and middleware

Setting Up Redis in Program.cs

Add the Redis connection to the DI container using StackExchange.Redis:

using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

// Register Redis connection
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
    ConnectionMultiplexer.Connect("localhost:6379")); // Update if needed

// Register services
builder.Services.AddScoped<RedisManager>();
builder.Services.AddScoped<ITokenService, TokenService>();

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

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseAuthorization();
app.MapControllers();

app.Run();

AuthController – Handling Login & Token Issuance

When a user logs in, we generate the JWT and refresh token, store them in Redis, and return them:

public class AuthController(ITokenService tokenService, RedisManager redisManager) : Controller
{
    private readonly ITokenService _tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
    private readonly RedisManager _redisManager = redisManager ?? throw new ArgumentNullException(nameof(redisManager));

    [AllowAnonymous]
    [HttpPost]
    [Route("Login")]
    public async Task<IActionResult> Post([FromBody] UserProfile value)
    {
        try
        {
            var authClaims = new List<Claim>
            {
                new(ClaimTypes.NameIdentifier, value.LoginId),
                new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new(ClaimTypes.Role, "Admin")
            };

            var l_refreshToken = _tokenService.GenerateRefreshToken();
            var l_jwtToken = _tokenService.GenerateAccessToken(authClaims);

            var data = new TokenModel 
            {
                AccessToken = l_jwtToken,
                RefreshToken = l_refreshToken,
                UserId = value.LoginId,
                AccessTokenExpiry = DateTime.Now.AddMinutes(15),
                RefreshTokenExpiry = DateTime.Now.AddHours(8),
                LastUpdate = DateTime.Now
            };

            await _redisManager.StoreTokenAsync(data);

            return Ok(new
            {
                userId = value.LoginId,
                accessToken = data.AccessToken,
                refreshToken = data.RefreshToken, 
            });
        }
        catch (Exception ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

RedisManager – Managing Token Cache

We use Redis hashes to store token details and set an expiry matching the refresh token lifespan.

public class RedisManager
{
    private readonly IDatabase _redisDb;

    public RedisManager(IConnectionMultiplexer redis)
    {
        _redisDb = redis.GetDatabase();
    }

    public async Task StoreTokenAsync(TokenModel model)
    {
        string redisKey = model.AccessToken;

        var entries = new HashEntry[]
        {
            new("accesstoken", model.AccessToken),
            new("accesstokenexpiry", model.AccessTokenExpiry.ToString("yyyy-MM-dd HH:mm:ss")),
            new("refreshtoken", model.RefreshToken),
            new("refreshtokenexpiry", model.RefreshTokenExpiry.ToString("yyyy-MM-dd HH:mm:ss")),
            new("userid", model.UserId),
            new("lastupdate", model.LastUpdate.ToString("yyyy-MM-dd HH:mm:ss"))
        };

        await _redisDb.HashSetAsync(redisKey, entries);
        await _redisDb.KeyExpireAsync(redisKey, model.RefreshTokenExpiry);
    }

    public async Task<TokenModel?> GetTokenAsync(string accessToken)
    {
        var entries = await _redisDb.HashGetAllAsync(accessToken);
        if (entries.Length == 0) return null;

        var dict = entries.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString());

        return new TokenModel
        {
            AccessToken = dict["accesstoken"],
            AccessTokenExpiry = Convert.ToDateTime(dict["accesstokenexpiry"]),
            RefreshToken = dict["refreshtoken"],
            RefreshTokenExpiry = Convert.ToDateTime(dict["refreshtokenexpiry"]),
            UserId = dict["userid"],
            LastUpdate = Convert.ToDateTime(dict["lastupdate"])
        };
    }

    public async Task<bool> RemoveTokenAsync(string accessToken)
    {
        return await _redisDb.KeyDeleteAsync(accessToken);
    }

    public async Task<bool> TokenExistsAsync(string accessToken)
    {
        return await _redisDb.KeyExistsAsync(accessToken);
    }

    public async Task<string?> GetUserIdFromTokenAsync(string accessToken)
    {
        return await _redisDb.HashGetAsync(accessToken, "userid");
    }
}

TokenModel – Token Metadata Structure

This model represents all the data needed to manage and validate JWT and refresh tokens:

public class TokenModel
{
    public string? AccessToken { get; set; }
    public DateTime AccessTokenExpiry { get; set; }
    public string? RefreshToken { get; set; }
    public DateTime RefreshTokenExpiry { get; set; }
    public string? UserId { get; set; }
    public DateTime LastUpdate { get; set; }
}

Advantages of Using Redis for Token Management

Feature Benefit
๐Ÿ”„ Fast access Redis provides O(1) time complexity for token reads/writes
๐Ÿ” Centralized validation Easily check tokens across distributed API instances
๐Ÿงผ Auto-expiry Keys auto-expire using KeyExpireAsync, reducing memory leaks
โšก High throughput Handles thousands of requests per second with minimal latency
๐Ÿ” Easy revocation Revoke any token by deleting its key from Redis

Conclusion

  • Using JWT with Redis Cache in ASP.NET Core is a great way to balance security, performance, and scalability. Redis acts as a high-speed lookup for token data, making authentication more efficient and production-ready.
  • This approach is especially powerful for large-scale applications, distributed systems, or microservices where central token validation is essential.