Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

OIDC Client Token Refresh

Access tokens have limited lifetimes for security. When using refresh tokens (obtained by requesting the offline_access scope), you can obtain new access tokens without requiring user interaction.

Use RefreshTokenAsync to manually refresh tokens:

var result = await client.RefreshTokenAsync(refreshToken);
if (result.IsError)
{
Console.WriteLine($"Refresh error: {result.Error}");
// Handle refresh failure - may need to re-authenticate
return;
}
// Use the new tokens
var newAccessToken = result.AccessToken;
var newRefreshToken = result.RefreshToken; // May be rotated
PropertyTypeDescription
AccessTokenstringThe new access token
IdentityTokenstringNew identity token (if issued)
RefreshTokenstringNew refresh token (if rotated)
ExpiresInintToken lifetime in seconds
AccessTokenExpirationDateTimeOffsetWhen the access token expires
IsErrorboolWhether the refresh failed
ErrorstringError code if failed
ErrorDescriptionstringError description if failed

Automatic Token Refresh with RefreshTokenDelegatingHandler

Section titled “Automatic Token Refresh with RefreshTokenDelegatingHandler”

For seamless API calls, use the RefreshTokenDelegatingHandler which automatically refreshes tokens before they expire:

// After login, create an HttpClient with automatic refresh
var loginResult = await client.LoginAsync();
var handler = new RefreshTokenDelegatingHandler(
client,
loginResult.AccessToken,
loginResult.RefreshToken
);
var apiClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com")
};
// Tokens are refreshed automatically when needed
var response = await apiClient.GetAsync("/protected-resource");

The LoginResult includes a pre-configured handler:

var loginResult = await client.LoginAsync();
if (!result.IsError && loginResult.RefreshTokenHandler != null)
{
var apiClient = new HttpClient(loginResult.RefreshTokenHandler);
// Use apiClient for API calls with automatic refresh
}

Subscribe to the TokenRefreshed event to be notified when tokens are refreshed:

var handler = new RefreshTokenDelegatingHandler(
client,
loginResult.AccessToken,
loginResult.RefreshToken
);
handler.TokenRefreshed += (sender, args) =>
{
// Persist the new tokens
SaveTokens(args.AccessToken, args.RefreshToken);
Console.WriteLine($"Tokens refreshed, new expiry in {args.ExpiresIn} seconds");
};
PropertyTypeDescription
AccessTokenstringThe new access token
RefreshTokenstringThe new refresh token
IdentityTokenstringNew identity token (if issued)
ExpiresInintToken lifetime in seconds

The handler exposes configuration properties:

var handler = new RefreshTokenDelegatingHandler(
client,
accessToken,
refreshToken,
tokenType: "Bearer", // Token type (default: Bearer)
innerHandler: new HttpClientHandler() // Custom inner handler
);
handler.Timeout = TimeSpan.FromSeconds(30); // Request timeout
PropertyTypeDescription
AccessTokenstringCurrent access token (read-only)
RefreshTokenstringCurrent refresh token (read-only)
TimeoutTimeSpanHTTP request timeout
  1. Store tokens securely - Use platform-specific secure storage (Keychain, Credential Manager, etc.)
  2. Handle refresh failures - Prompt for re-authentication when refresh fails
  3. Use automatic refresh - The RefreshTokenDelegatingHandler simplifies token management
  4. Persist rotated tokens - Subscribe to TokenRefreshed to save new tokens immediately