Issue tokens concurrently with synchronization per client
The pattern looks correct. One disadvantage here is you're blocking threads waiting for the lock. You could instead use a SemaphoreSlim and its WaitAsync method for a nonblocking lock.

Even better, have a look at LazyCache. (Scott Hanselman blogged about this recently.) It basically does all of this for you - caching, asynchronous double-check locking, and guaranteeing refresh only happens once. You could replace most of what you've done here with a GetTokenAsync implementation that looks something like this:

return await _tokenCache.GetOrAddAsync(
    $"tokens-{client.ClientId}",
    () =&gt; client.BaseAddress
        .AppendPathSegment("/login")
        .WithBasicAuth(client.Username, client.Password)
        .GetJsonAsync&lt;TokenDetails&gt;(),
    TimeSpan.FromHours(1));


One little caveat here is you need to know ahead of time how long to cache the token, i.e. you can't use the Expires value returned from the server. But in your case it sounds like you know it's an hour, so hopefully that's a non-issue.