feat: Cache tokens between calls (#19)
This commit is contained in:
@@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user