Token manager
Fortnox access tokens live about an hour, and the refresh token is single-use — once you swap it for a new pair, the old one is dead. That makes concurrent refresh races a real problem: two threads both notice the token is stale, both try to refresh, one of them gets a 401 because the refresh token was already consumed.
IFortnoxTokenManager solves this with per-key in-memory caching + per-key locking + proactive refresh.
Usage
public class MyFortnoxService(IFortnoxTokenManager tokenManager)
{
public async Task DoWorkAsync(string tenantId)
{
var result = await tokenManager.GetAccessTokenAsync(
tenantId,
loadToken: () => LoadTokenFromDb(tenantId),
saveToken: token => SaveTokenToDb(tenantId, token));
if (!result.IsSuccess)
{
// The token may need re-authorization (refresh chain broken).
return;
}
var accessToken = result.Value;
// ... use accessToken for Fortnox REST calls
}
}
How it works
- Cache lookup. If the key has a cached token whose
ExpireTimeis more thanRefreshBufferSecondsaway, return it immediately. No I/O. - Acquire the per-key lock. Other threads asking for the same key wait here. Different keys do not contend.
- Recheck after acquiring the lock. Another thread may have refreshed while we waited.
- Load from storage. Call your
loadTokendelegate. - Validate. If the loaded token is still fresh, cache it and return.
- Refresh. If stale, call
IFortnoxConnectionService.RefreshTokenAsyncwith the loaded refresh token. - Persist + cache. Call your
saveTokendelegate with the newTokenData, store it in the cache, return the access token.
The lock is released regardless of outcome. Errors propagate as Result.Fail(...).
Configuration
builder.Services.AddThargaFortnox(o =>
{
o.ClientId = "...";
o.ClientSecret = "...";
o.RedirectUri = new Uri("...");
o.TokenManager = new TokenManagerOptions
{
RefreshBufferSeconds = 120 // refresh 2 minutes before expiry
};
});
Default RefreshBufferSeconds = 60. Increase it if your network round-trips are slow or if you want larger safety margin for long-running operations that started just before expiry.
Multiple tenants
Each key is an independent connection. Pass whatever identifier ties the token to its owner — typically a company id, tenant id, or CustomerNumber. The manager isolates locks and cache entries by key, so one tenant's refresh never blocks another's.
await tokenManager.GetAccessTokenAsync("tenant-a", ...); // independent
await tokenManager.GetAccessTokenAsync("tenant-b", ...); // from each other
Limitation: single-instance only
The cache is in-process. If you run multiple server instances against the same Fortnox connection, instance A's refresh invalidates the refresh token instance B has cached — the next call from B fails because Fortnox no longer recognises B's now-old refresh token.
For multi-instance deployments either:
- Run a single dedicated worker that owns the refresh, and have other instances request access tokens from it; or
- Coordinate refresh via a distributed lock + shared store (the
loadToken/saveTokendelegates already give you the seam — point them at a row in your shared database, and add a distributed lock around the refresh).
Tharga.Fortnox deliberately keeps this out of scope: the right shared store is project-specific (SQL, Redis, MongoDB, etc.), and pretending otherwise would mean shipping a dependency on whichever we picked.