In a typical scenario, a user logs in and the server issues a JWT access token. As the user works in the application, the token expires and they are suddenly logged out. This sequence leads to a poor user experience, with frequent logins, and creates a temptation to increase token lifetimes, which significantly increases security risks.
This creates a difficult trade-off: short-lived tokens are secure but inconvenient, while long-lived tokens are convenient but insecure. However, refresh tokens solve this problem by maintaining security without interrupting the user session. How? Let's see that in this article.
Solution
A refresh token is a long-lived credential used solely to obtain new access tokens without requiring the user to re-authenticate. The high-level flow begins when a user logs in successfully and the server issues both a short-lived access token and a long-lived refresh token. While the client uses the access token for standard API calls, that token eventually expires, triggering the application to take action in the background.
When the access token becomes invalid, the client sends the refresh token back to the server for validation. Once the server confirms that the refresh token is valid and has not been revoked, it issues a new access token to the client. This entire exchange happens seamlessly, allowing the user to continue their session without interruption or the need to re-enter credentials manually.
Where does it reside?
A refresh token is intentionally stored in two places.
On the client side, browser-based applications should store refresh tokens in HttpOnly cookies rather than localStorage. This approach protects the token from Cross-Site Scripting (XSS) by preventing JavaScript access, and the use of Secure and SameSite attributes ensures secure transmission.
On the server side, persisting refresh tokens in a database is critical for maintaining session control. By tracking fields such as the user ID, hashed token, expiration date, and revocation status, the server can manually invalidate sessions during a logout or password change. This centralized management also allows the system to detect and block suspicious activity, such as token reuse or session hijacking.
Example:
To implement refresh token support manually,
start by creating a data model to store these credentials in your database. In this example, the class is named UserRefreshToken to clearly distinguish it from the short-lived access token.
public class UserRefreshToken
{
public int Id { get; set; }
public string TokenValue { get; set; }
public string OwnerId { get; set; }
public IdentityUser Owner { get; set; }
public DateTime ExpirationTime { get; set; }
public bool IsActive { get; set; } = true;
}
Next, register this model within your data context so that the database can persist the tokens:
public class ApiSecurityDbContext : IdentityDbContext
{
public ApiSecurityDbContext(DbContextOptions<ApiSecurityDbContext> options)
: base(options) { }
public DbSet<UserRefreshToken> AuthRefreshTokens { get; set; }
}
During the authentication process, generate a secure, unique string to serve as the refresh token and save it to your database. This logic ensures the token is cryptographically strong and linked to the specific user:
public async Task<string> CreatePersistentTokenAsync(IdentityUser user)
{
var newRefreshToken = new UserRefreshToken
{
TokenValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
OwnerId = user.Id,
ExpirationTime = DateTime.UtcNow.AddDays(7),
IsActive = true
};
_context.AuthRefreshTokens.Add(newRefreshToken);
await _context.SaveChangesAsync();
return newRefreshToken.TokenValue;
}
When a user successfully logs in, the API should return both the short-lived JWT and the persistent refresh token. This allows the client to store the refresh token and use it to request a new session once the initial JWT expires:
var jwtToken = GenerateJwt(user); // Standard JWT generation
var persistentToken = await CreatePersistentTokenAsync(user);
return Ok(new
{
AuthToken = jwtToken,
RefreshToken = persistentToken
});
Finally, when the client detects that the JWT has expired, it can send the stored refresh token to a dedicated endpoint, such as /api/auth/refresh-session. The server then validates the token’s status and expiration before issuing a fresh JWT to maintain the user's session without interruption.
Conclusion
In this article, we have seen how refresh tokens balance security and usability by allowing seamless session renewals without compromising API safety. Hope you find this useful.