Bradley Wells
Posted on December 20, 2019
In this tutorial, you will add access token caching to your IdentityServer4 protected API in order to reduce unnecessary load on your authentication server.
Be sure you are caught up by reviewing Part 1 and Part 2 of this tutorial series.
In this Blazor tutorial series
- Part 1: Blazor with Web API Solution Structure
- Part 2: Consume API protected by IdentityServer4
- Part 3: Cache IdentityServer4 API Access Token (Coming Soon)
- Part 4: The Easy Way (Coming Soon)
Configure Access Token Cache
Start by launching the BlazorContacts solution. In the Solution Explorer, expand the BlazorContacts.Web project. Add a new class to the Services directory and call it ApiTokenCacheService.cs.
Add a few using
directives in order to reference some namespaces you will be using in this class.
using IdentityModel.Client;
using Microsoft.Extensions.Caching.Distributed;
using System.Net.Http;
using System.Text.Json;
Next, declare some variables and build the constructor for this service. In this example, we will cache the access token for 1
day.
private readonly HttpClient _httpClient;
private static readonly Object _lock = new Object();
private IDistributedCache _cache;
private const int cacheExpirationInDays = 1;
private class AccessTokenItem
{
public string AccessToken { get; set; } = string.Empty;
public DateTime Expiry { get; set; }
}
public ApiTokenCacheService(
IHttpClientFactory httpClientFactory,
IDistributedCache cache)
{
_httpClient = httpClientFactory.CreateClient();
_cache = cache;
}
We need to define a method that will follow the following logic: It should first check if there is a valid access token in the cache. If there is, it should return that token. If there is not, it should request a new token, add the new token to the cache, and return the token. Take a look at the following.
public async Task<string> GetAccessToken(string client_name, string api_scope, string secret)
{
AccessTokenItem accessToken = GetFromCache(client_name);
if (accessToken != null && accessToken.Expiry > DateTime.UtcNow)
{
return accessToken.AccessToken;
}
// Token not cached, or token is expired. Request new token from auth server
AccessTokenItem newAccessToken = await RequestNewToken(client_name, api_scope, secret);
AddToCache(client_name, newAccessToken);
return newAccessToken.AccessToken;
}
Notice, I went ahead and abstracted this method by passing in relevant values as arguments instead of harcoding them in the method. This will be useful later if I ever decide to use this caching service to fetch access tokens from other APIs.
There are three methods referenced above that we have not yet defined, GetFromCache()
, RequestNewToken()
, and AddToCache()
.
You previously declared an interface for a DistributedCache
. Use this cache to fetch a value by key.
private AccessTokenItem GetFromCache(string key)
{
var item = _cache.GetString(key);
if (item != null)
{
return JsonSerializer.Deserialize<AccessTokenItem>(item);
}
return null;
}
This method uses the System.Text.Json
serializer to deserialize and return an AccessTokenItem
object retrieved from _cache
.
The AddToCache()
method should look similar to the following.
private void AddToCache(string key, AccessTokenItem accessTokenItem)
{
var options = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromDays(cacheExpirationInDays));
lock (_lock)
{
_cache.SetString(key, JsonSerializer.Serialize(accessTokenItem), options);
}
}
To customize the cache timeout, feel free to modify the DistributedCacheEntryOptions
by using TimeSpan.FromHours
instead of FromDays
, for example.
Finally, take the RequestNewToken()
method you previously included in ApiService.cs and move it to ApiTokenCacheService.cs. This will ensure all access token related code is in the same place, making the code more manageable. I have made a couple small changes, because this method should return an AccessTokenItem
, which includes expiration date, instead of just the access token string
we used in the previous lesson.
private async Task<AccessTokenItem> RequestNewToken(string client_name, string api_scope, string secret)
{
try
{
var discovery = await HttpClientDiscoveryExtensions.GetDiscoveryDocumentAsync(
_httpClient, "http://localhost:5000");
if (discovery.IsError)
{
throw new ApplicationException($"Error: {discovery.Error}");
}
var tokenResponse = await HttpClientTokenRequestExtensions.RequestClientCredentialsTokenAsync(_httpClient, new ClientCredentialsTokenRequest
{
Scope = api_scope,
ClientSecret = secret,
Address = discovery.TokenEndpoint,
ClientId = client_name
});
if (tokenResponse.IsError)
{
throw new ApplicationException($"Error: {tokenResponse.Error}");
}
return new AccessTokenItem
{
Expiry = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
AccessToken = tokenResponse.AccessToken
};
}
catch (Exception e)
{
throw new ApplicationException($"Exception {e}");
}
}
Register Caching Service
Because the access token, in our configuration, is granted at the application level, the same token will be used for all users of the application. By registering the caching service as a Singleton service, each user of the authorized Blazor application, will be able to use the access token, whether it be from the cache or a new token.
Open Startup.cs of BlazorContacts.Web to register the access token caching service. In the ConfigureServices() method, add the following line:
services.AddSingleton<Services.ApiTokenCacheService>();
Inject Service as Dependency
Now that ApiTokenCacheService
is registered, you can inject it as a dependency of the ApiService
service. Modify ApiService.cs
public HttpClient _httpClient;
private readonly ApiTokenCacheService _apiTokenService;
public ApiService(HttpClient client, ApiTokenCacheService apiTokenCacheService)
{
_httpClient = client;
_apiTokenService = apiTokenCacheService;
}
Each time you make an API call, you must get the access token, either from cache or from the auth server, and set it as the bearer token for the HttpClient
making the call. Your GetContactsAsync()
method may look like the following:
public async Task<List<Contact>> GetContactsAsync()
{
var access_token = await _apiTokenService.GetAccessToken(
"blazorcontacts-web",
"blazorcontacts-api",
"thisismyclientspecificsecret"
);
_httpClient.SetBearerToken(access_token);
var response = await _httpClient.GetAsync("api/contacts");
response.EnsureSuccessStatusCode();
using var responseContent = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<List<Contact>>(responseContent);
}
The Bottom Line
In this tutorial, you learned how to cache access tokens for efficient API calls. You created a service class that checks for an existing token in the cache. If a valid token is not cached, your service fetched a new token from the authentication service and added the new token to the cache. In the next tutorial, you will learn an easy way to transparently manage access tokens in .NET Core 3.1 applications.
Source code for this project is available on GitHub.
Posted on December 20, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.