-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathClientCredentialsTokenManager.cs
More file actions
152 lines (130 loc) · 6.31 KB
/
ClientCredentialsTokenManager.cs
File metadata and controls
152 lines (130 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Duende.AccessTokenManagement.OTel;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Duende.AccessTokenManagement.Internal;
internal class ClientCredentialsTokenManager(
AccessTokenManagementMetrics metrics,
IOptions<ClientCredentialsTokenManagementOptions> options,
[FromKeyedServices(ServiceProviderKeys.ClientCredentialsTokenCache)]
HybridCache cache,
TimeProvider time,
IClientCredentialsTokenEndpoint client,
IClientCredentialsCacheKeyGenerator cacheKeyGenerator,
IClientCredentialsCacheDurationStore cacheDurationAutoTuningStore,
ILogger<ClientCredentialsTokenManager> logger
) : IClientCredentialsTokenManager
{
// A flag that's written into the Data property of exceptions to distinguish
// between exceptions that are thrown inside the cache and those that are thrown
// inside the factory.
private const string ThrownInsideFactoryExceptionKey = "Duende.AccessTokenManagement.ThrownInside";
private readonly ClientCredentialsTokenManagementOptions _options = options.Value;
public async Task<TokenResult<ClientCredentialsToken>> GetAccessTokenAsync(
ClientCredentialsClientName clientName,
TokenRequestParameters? parameters = null,
CT ct = default)
{
var cacheKey = cacheKeyGenerator.GenerateKey(clientName, parameters);
parameters ??= new TokenRequestParameters();
var cacheExpiration = cacheDurationAutoTuningStore.GetExpiration(cacheKey);
// On force renewal, don't read from the cache, so we always get a new token.
var disableDistributedCacheRead = parameters.ForceTokenRenewal
? HybridCacheEntryFlags.DisableLocalCacheRead | HybridCacheEntryFlags.DisableDistributedCacheRead
: HybridCacheEntryFlags.None; // Even with "none", we still get cache stampede protection :)
var entryOptions = new HybridCacheEntryOptions()
{
Expiration = cacheExpiration,
LocalCacheExpiration = _options.LocalCacheExpiration,
Flags = disableDistributedCacheRead,
};
ClientCredentialsToken token;
try
{
token = await cache.GetOrCreateAsync(
key: cacheKey.ToString(),
factory: async (c) => await RequestToken(cacheKey, clientName, parameters, c),
options: entryOptions,
tags: [HybridCacheConstants.CacheTag, clientName.ToString()],
cancellationToken: ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (PreventCacheException ex)
{
// This exception is thrown if there was a failure while retrieving an access token. We
// don't want to cache this failure, so we throw an exception to bypass the cache action.
logger.WillNotCacheTokenResultWithError(LogLevel.Debug, clientName, ex.Failure.Error,
ex.Failure.ErrorDescription);
return ex.Failure;
}
catch (Exception ex) when (!ex.Data.Contains(ThrownInsideFactoryExceptionKey))
{
// if there was an exception in the cache, we'll just retry without the cache and hope for the best
logger.ExceptionWhileReadingFromCache(LogLevel.Warning, ex, clientName);
token = await RequestToken(cacheKey, clientName, parameters, ct);
}
// Check if token has expired. Ideally, the cache lifetime auto-tuning should prevent this,
// but for the first request OR if the token lifetime is changed, we might end up here.
if (!parameters.ForceTokenRenewal
&& token.Expiration - TimeSpan.FromSeconds(_options.CacheLifetimeBuffer) <= time.GetUtcNow())
{
// retry the request, but force a renewal
var tokenResult = await GetAccessTokenAsync(clientName, parameters with
{
ForceTokenRenewal = true
}, ct);
if (!tokenResult.Succeeded)
{
return tokenResult.FailedResult;
}
token = tokenResult.Token;
}
metrics.AccessTokenUsed(token.ClientId, AccessTokenManagementMetrics.TokenRequestType.ClientCredentials);
return token;
}
private async Task<ClientCredentialsToken> RequestToken(ClientCredentialsCacheKey cacheKey,
ClientCredentialsClientName clientName, TokenRequestParameters parameters, CT ct)
{
TokenResult<ClientCredentialsToken> tokenResult;
try
{
tokenResult = await client.RequestAccessTokenAsync(clientName, parameters, ct);
}
catch (Exception ex)
{
// If there is a problem with retrieving data, then we want to bubble this back to the client.
// However, we want to distinguish this from exceptions that happen inside the cache itself.
// So, any exception that happens internally gets a special flag.
ex.Data[ThrownInsideFactoryExceptionKey] = true;
throw;
}
if (!tokenResult.WasSuccessful(out var token, out var failure))
{
// Unfortunately, hybrid cache has no clean way to prevent failures from being cached.
// So we have to use an exception here.
throw new PreventCacheException(failure);
}
// See if we need to record how long this access token is valid, to be used the next time
// this access token is used.
var cacheDuration = cacheDurationAutoTuningStore.SetExpiration(cacheKey, token.Expiration);
logger.CachingAccessToken(LogLevel.Debug, clientName, cacheDuration);
return token;
}
public async Task DeleteAccessTokenAsync(ClientCredentialsClientName clientName,
TokenRequestParameters? parameters = null,
CT ct = default)
{
var cacheKey = cacheKeyGenerator.GenerateKey(clientName, parameters);
await cache.RemoveAsync(cacheKey.ToString(), ct);
}
internal class PreventCacheException(FailedResult failure) : Exception
{
public FailedResult Failure { get; } = failure;
}
}