feat: Cache tokens between calls (#19)

This commit is contained in:
Valters Melnalksnis
2022-06-16 20:22:52 +03:00
parent da99d06272
commit d391cb7721
8 changed files with 135 additions and 10 deletions
@@ -30,13 +30,16 @@ public static class ServiceCollectionExtensions
/// <summary>Adds all required services for <see cref="INordigenClient"/>.</summary>
/// <param name="serviceCollection">The service collection in which to register the services.</param>
/// <param name="configuration">The configuration to which to bind options models.</param>
/// <param name="clock">Clock for accessing the current time.</param>
/// <param name="dateTimeZoneProvider">Time zone provider for date and time serialization.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> for the <see cref="HttpClient"/> used by <see cref="INordigenClient"/>.</returns>
public static IHttpClientBuilder AddNordigenDotNet(
this IServiceCollection serviceCollection,
IConfiguration configuration,
IClock clock,
IDateTimeZoneProvider dateTimeZoneProvider)
{
serviceCollection.TryAddSingleton(clock);
serviceCollection.TryAddSingleton(dateTimeZoneProvider);
return serviceCollection.AddNordigenDotNet(configuration);
}
@@ -58,6 +61,7 @@ public static class ServiceCollectionExtensions
serviceCollection.AddHttpClient<TokenDelegatingHandler>(ConfigureNordigenClient);
return serviceCollection
.AddSingleton<NordigenTokenCache>()
.AddTransient<INordigenClient, NordigenClient>()
.AddTransient<IAccountClient, AccountClient>()
.AddTransient<IAgreementClient, AgreementClient>()
@@ -24,4 +24,7 @@ public sealed record NordigenOptions
/// <summary>Gets the secret key used to create new access tokens.</summary>
[Required]
public string SecretKey { get; init; } = null!;
/// <summary>Gets the factor by which to divide the expiration time for access and refresh tokens.</summary>
public double ExpirationFactor { get; init; } = 2d;
}
@@ -0,0 +1,59 @@
// Copyright 2022 Valters Melnalksnis
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using NodaTime;
namespace VMelnalksnis.NordigenDotNet.Tokens;
/// <summary>Caches refresh and access tokens for <see cref="NordigenHttpClient"/>.</summary>
public sealed class NordigenTokenCache
{
private readonly NordigenOptions _nordigenOptions;
private readonly IClock _clock;
/// <summary>Initializes a new instance of the <see cref="NordigenTokenCache"/> class.</summary>
/// <param name="nordigenOptions">Options for connection to the Nordigen API.</param>
/// <param name="clock">Clock for accessing the current time.</param>
public NordigenTokenCache(NordigenOptions nordigenOptions, IClock clock)
{
_nordigenOptions = nordigenOptions;
_clock = clock;
}
internal AccessToken? AccessToken { get; private set; }
internal bool IsAccessExpired =>
AccessToken is not null &&
AccessExpiresAt is not null &&
_clock.GetCurrentInstant() > AccessExpiresAt;
internal Token? Token { get; private set; }
internal bool IsRefreshExpired =>
Token is not null &&
RefreshExpiresAt is not null &&
_clock.GetCurrentInstant() > RefreshExpiresAt;
private Instant? AccessExpiresAt { get; set; }
private Instant? RefreshExpiresAt { get; set; }
internal void SetToken(Token token)
{
Token = token;
AccessToken = token;
var currentInstant = _clock.GetCurrentInstant();
var factor = _nordigenOptions.ExpirationFactor;
AccessExpiresAt = currentInstant + Duration.FromSeconds(token.AccessExpires / factor);
RefreshExpiresAt = currentInstant + Duration.FromSeconds(token.RefreshExpires / factor);
}
internal void SetAccessToken(AccessToken accessToken)
{
AccessToken = accessToken;
var currentInstant = _clock.GetCurrentInstant();
var factor = _nordigenOptions.ExpirationFactor;
AccessExpiresAt = currentInstant + Duration.FromSeconds(accessToken.AccessExpires / factor);
}
}
@@ -2,6 +2,7 @@
// Licensed under the Apache License 2.0.
// See LICENSE file in the project root for full license information.
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
@@ -22,14 +23,17 @@ public sealed class TokenDelegatingHandler : DelegatingHandler
private readonly HttpClient _httpClient;
private readonly NordigenOptions _nordigenOptions;
private readonly NordigenTokenCache _tokenCache;
/// <summary>Initializes a new instance of the <see cref="TokenDelegatingHandler"/> class.</summary>
/// <param name="httpClient">Http client configured for making requests to the Nordigen API.</param>
/// <param name="nordigenOptions">Options for connection to the Nordigen API.</param>
public TokenDelegatingHandler(HttpClient httpClient, NordigenOptions nordigenOptions)
/// <param name="tokenCache">Nordigen API token cache for preserving tokens between requests.</param>
public TokenDelegatingHandler(HttpClient httpClient, NordigenOptions nordigenOptions, NordigenTokenCache tokenCache)
{
_httpClient = httpClient;
_nordigenOptions = nordigenOptions;
_tokenCache = tokenCache;
}
/// <inheritdoc />
@@ -37,13 +41,27 @@ public sealed class TokenDelegatingHandler : DelegatingHandler
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await CreateNewToken().ConfigureAwait(false);
request.Headers.Authorization = new("Bearer", token.Access);
if (_tokenCache.AccessToken is null || _tokenCache.IsRefreshExpired)
{
await CreateNewToken().ConfigureAwait(false);
}
else if (_tokenCache.IsAccessExpired)
{
await RefreshToken().ConfigureAwait(false);
}
request.Headers.Authorization = new("Bearer", _tokenCache.AccessToken?.Access);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode is not HttpStatusCode.Unauthorized)
{
return response;
}
await CreateNewToken().ConfigureAwait(false);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
private async Task<Token> CreateNewToken()
private async Task CreateNewToken()
{
var tokenCreation = new TokenCreation(_nordigenOptions.SecretId, _nordigenOptions.SecretKey);
var tokenResponse = await _httpClient
@@ -52,18 +70,18 @@ public sealed class TokenDelegatingHandler : DelegatingHandler
await tokenResponse.ThrowIfNotSuccessful().ConfigureAwait(false);
var token = await tokenResponse.Content.ReadFromJsonAsync(_tokenInfo).ConfigureAwait(false);
return token!;
_tokenCache.SetToken(token!);
}
private async Task<AccessToken> Refresh(TokenRefresh tokenRefresh)
private async Task RefreshToken()
{
var tokenResponse = await _httpClient
.PostAsJsonAsync(Routes.Tokens.Refresh, tokenRefresh, _tokenRefreshInfo)
.PostAsJsonAsync(Routes.Tokens.Refresh, new(_tokenCache.Token!.Refresh), _tokenRefreshInfo)
.ConfigureAwait(false);
await tokenResponse.ThrowIfNotSuccessful().ConfigureAwait(false);
var accessToken = await tokenResponse.Content.ReadFromJsonAsync(_accessTokenInfo).ConfigureAwait(false);
return accessToken!;
_tokenCache.SetAccessToken(accessToken!);
}
}