Nobody wants to sign in every hour. Yet that’s exactly what happens when access tokens expire in applications without proper token management. The good news? We can fix this by implementing automatic token renewal in our BFF!
This is a big topic, so I’ve split it into multiple parts. You can jump to the section you need, but for background and context, it’s best to start here:
Part 1 – Introduction
Part 2 – Introducing the Backend-for-Frontend (BFF) pattern
Part 3 – Securing the Cookie Session
Part 4 – Implementing a BFF in ASP.NET Core
Part 5 – Automatic Token Renewal
Part 6 – Securing the BFF using CORS (coming soon)
Part 7 – Introducing the Duende BFF Library (coming soon)
The Problem
Our implementation works perfectly for the lifetime of an access token. The catch? That lifetime varies widely based on your identity provider configuration: some use 10-minute tokens for high security, others allow 24 hours or more. Regardless of the duration, when tokens expire, users get logged out. Let’s fix that.
How do we get a new access token?
Introducing the Refresh Token
When we authenticate with OpenID Connect, we can request a special long-lived credential called a refresh token. This token has one job: obtaining new access tokens when the current one expires.
Here’s how it works:
- During initial login, we request the offline_access scope.
- The identity provider returns three tokens: ID token, access token, and refresh token.
- When the access token expires, we use the refresh token to request a fresh set.
- The user stays logged in without knowing any of this happened.
The refresh token typically lasts much longer (days or weeks) and can be used multiple times to keep renewing access tokens until the user explicitly logs out, or when the refresh token itself expires.
The Token Renewal Implementation Challenge
By requesting the offline_access scope during authentication, we will receive a refresh token. However, ASP.NET Core has no built-in mechanism to automatically use it when access tokens expire.
When an access token expires, the API responds with:
HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/10.0
WWW-Authenticate: Bearer error="invalid_token", error_description="The token expired at '06/24/2025 09:24:46'"
Date: Tue, 24 Jun 2025 09:24:47 GMT
Content-Length: 0
So how do we implement automatic renewal? Let’s explore that next.
Using the Duende Access Token Management Library
Instead of building token renewal from scratch, we’ll use the production-tested Duende.AccessTokenManagement library. This open-source solution handles the complex security requirements of token management, including automatic renewal with refresh tokens.
Why Not Build It Yourself?
Token management looks simple but hides serious pitfalls:
- Race conditions when multiple requests need renewal simultaneously.
- Token storage conflicts in multi-instance deployments.
- Security vulnerabilities from improper token handling.
Real-world incidents have occurred where custom implementations accidentally exposed tokens to wrong users or leaked credentials through caching errors. These aren’t theoretical risks, instead they’re expensive mistakes that happen when security-critical code is built in-house without extensive testing.
Getting Started
The library is well-documented and actively maintained:
By using this battle-tested library, you get reliable token management without the risk and maintenance burdens of custom code.
Implementing the Access Token Management Library
Once the package is installed, integrating the library into your application requires just a few lines of code. Register and configure it in your Program.cs:

Once the package is installed, integrating the library into your application requires just a few lines of code. Register and configure it in your Program.cs:
builder.Services.AddOpenIdConnectAccessTokenManagement(o =>
{
o.RefreshBeforeExpiration = TimeSpan.FromSeconds(15);
});
The RefreshBeforeExpiration setting controls when the library proactively refreshes tokens before they expire. In this example, we’re using 15 seconds to demonstrate the renewal process in action. For production applications, you’ll typically want a longer buffer, anywhere from 1-5 minutes depending on your API response time requirements and token lifetime.
Updating the ApiController
Now we need to update the ApiController to use the access token management service. Start by injecting the service through the constructor:
public class ApiController : ControllerBase
{
private readonly HttpClient _httpClient;
private readonly IUserTokenManagementService _tokenManagement;
public ApiController(HttpClient httpClient,
IUserTokenManagementService tokenManagement)
{
_httpClient = httpClient;
_tokenManagement = tokenManagement;
}
...
}
Next, replace the existing logic that retrieves the access token with the new implementation using the token management service:
// Get access token using Duende Access Token Management
// (handles automatic refresh)
var tokenResult = await _tokenManagement.GetAccessTokenAsync(User);
if (tokenResult.IsError)
{
return StatusCode((int)HttpStatusCode.Unauthorized, new
{
error = "Failed to get access token",
details = $"Token error: {tokenResult.Error}",
statusCode = 401,
tokenManagementUsed = true
});
}
// Create HTTP request with Authorization header
var url = "https://www.secure.nu/tokenapi/gettime";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var authHeader = new AuthenticationHeaderValue("Bearer",
tokenResult.AccessToken);
request.Headers.Authorization = authHeader;
Using the Access Token Management library
With the setup complete, you can now run the application and make calls to the remote API. In Fiddler, you will see that each request from the browser to the BFF triggers a corresponding call to the remote API, as expected.

When the access token is about to expire or has become invalid, the Access Token Management library automatically requests a new token from the authorization server (in this case, Duende IdentityServer). This process is handled entirely by the library, so you do not need to write any manual logic for handling token expiration or refresh.
You can observe this behavior in the server logs when a token refresh occurs:
trce: Duende.UserAccessAccessTokenManagementService
Starting user token acquisition
dbug: Duende.UserAccessAccessTokenManagementService
Token for user Bob Smith will be refreshed.
Expiration: 06/24/2025 11:37:41 +00:00,
ForceRenewal:False
trce: Duende.UserTokenEndpointService
Refreshing access token using refresh token:
hash=blsquoA1xJkBGCxQ2kqM4w
dbug: Duende.UserTokenEndpointService
Sending Refresh token request to:
https://identityservice.secure.nu/connect/token
dbug: Duende.UserTokenEndpointService
Access Token of type Bearer refreshed with expiration:
06/24/2025 11:38:12 +00:00
trce: Duende.UserAccessAccessTokenManagementService
Returning refreshed token for user: Bob Smith
info: System.Net.Http.HttpClient.Default.LogicalHandler
Start processing HTTP request
GET https://www.secure.nu/tokenapi/gettime
info: System.Net.Http.HttpClient.Default.ClientHandler
Sending HTTP request
GET https://www.secure.nu/tokenapi/gettime
info: System.Net.Http.HttpClient.Default.ClientHandler
Received HTTP response headers after 42.5267ms - 200
info: System.Net.Http.HttpClient.Default.LogicalHandler
End processing HTTP request after 47.1398ms - 200
Summary
Adding automatic access token renewal to the BFF was straightforward, thanks to the Access Token Management library. You can review the complete implementation for this step in the 3-TokenRenewal project. The full source code for the entire series is available on GitHub.
This enhancement ensures users stay authenticated without interruption, while the library handles all the token management behind the scenes.
What’s next?
With OpenID Connect and automatic access token management in place, our BFF setup is nearly complete. However, before it is truly production-ready, there are still important security measures to address.
The next step is to implement proper CORS (Cross-Origin Resource Sharing) support to help protect your application from cross-origin threats. This will be the focus of the next blog post. Stay tuned!
(coming soon)