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.