|
5 | 5 | using System.Globalization; |
6 | 6 | using System.Threading.Tasks; |
7 | 7 | using Microsoft.Extensions.Caching.Distributed; |
| 8 | +using Microsoft.Extensions.Caching.Memory; |
8 | 9 | using Microsoft.Extensions.DependencyInjection; |
9 | 10 | using Microsoft.Extensions.Logging; |
| 11 | +using Microsoft.Extensions.Options; |
10 | 12 | using Microsoft.Identity.Client; |
11 | 13 | using Microsoft.Identity.Client.Cache; |
12 | 14 | using Microsoft.Identity.Web.Test.Common; |
13 | 15 | using Microsoft.Identity.Web.Test.Common.Mocks; |
14 | 16 | using Microsoft.Identity.Web.TokenCacheProviders.Distributed; |
| 17 | +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; |
15 | 18 | using Microsoft.IdentityModel.Abstractions; |
16 | 19 | using Xunit; |
17 | 20 |
|
@@ -190,6 +193,111 @@ public async Task SingletonMsal_ResultsInCorrectCacheEntries_Test() |
190 | 193 | } |
191 | 194 | } |
192 | 195 |
|
| 196 | + #region CacheKeyExtensibility test |
| 197 | + private const int TokenCacheMemoryLimitInMb = 100; |
| 198 | + private static MemoryCache s_memoryCache = InitiatlizeMemoryCache(); |
| 199 | + |
| 200 | + private static MemoryCache InitiatlizeMemoryCache() |
| 201 | + { |
| 202 | + // For 100 MB limit ... ~2KB per token entry means 50,000 entries |
| 203 | + var options = Options.Create(new MemoryCacheOptions() { SizeLimit = (TokenCacheMemoryLimitInMb / 2) * 1000 }); |
| 204 | + var cache = new MemoryCache(options); |
| 205 | + |
| 206 | + return cache; |
| 207 | + } |
| 208 | + |
| 209 | + /// <summary> |
| 210 | + /// Token cache for MSAL based on MemoryCache, which can be partitioned by an additional key. |
| 211 | + /// For app tokens, the default key is ClientID + TenantID (and MSAL also looks for resource). |
| 212 | + /// </summary> |
| 213 | + private class PartitionedMsalTokenMemoryCacheProvider : MsalMemoryTokenCacheProvider |
| 214 | + { |
| 215 | + private readonly string? _cacheKeySuffix; |
| 216 | + |
| 217 | + /// <summary> |
| 218 | + /// Ctor |
| 219 | + /// </summary> |
| 220 | + /// <param name="memoryCache">A memory cache which can be configured for max size etc.</param> |
| 221 | + /// <param name="cacheOptions">Additional cache options, which canbe ignored for app tokens.</param> |
| 222 | + /// <param name="cachePartition">An aditional partition key. If let null, the original cache scoping is used (clientID, tenantID). MSAL also looks for resource.</param> |
| 223 | + public PartitionedMsalTokenMemoryCacheProvider( |
| 224 | + IMemoryCache memoryCache, |
| 225 | + IOptions<MsalMemoryTokenCacheOptions> cacheOptions, |
| 226 | + string? cachePartition) : base(memoryCache, cacheOptions) |
| 227 | + { |
| 228 | + _cacheKeySuffix = cachePartition; |
| 229 | + } |
| 230 | + |
| 231 | + public override string GetSuggestedCacheKey(TokenCacheNotificationArgs args) |
| 232 | + { |
| 233 | + return base.GetSuggestedCacheKey(args) + (_cacheKeySuffix ?? ""); |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + private async Task<AuthenticationResult> GetTokensAssociatedWithKey(string? cachePartition, bool expectCacheHit) |
| 238 | + { |
| 239 | + MockHttpMessageHandler? handler = null; |
| 240 | + MockHttpClientFactory? mockHttpClient = null; |
| 241 | + try |
| 242 | + { |
| 243 | + |
| 244 | + if (expectCacheHit == false) |
| 245 | + { |
| 246 | + mockHttpClient = new MockHttpClientFactory(); |
| 247 | + handler = mockHttpClient.AddMockHandler(MockHttpCreator.CreateClientCredentialTokenHandler()); |
| 248 | + } |
| 249 | + |
| 250 | + var msalMemoryTokenCacheProvider = |
| 251 | + new PartitionedMsalTokenMemoryCacheProvider( |
| 252 | + s_memoryCache, |
| 253 | + Options.Create(new MsalMemoryTokenCacheOptions()), |
| 254 | + cachePartition: cachePartition); |
| 255 | + |
| 256 | + var confidentialApp = ConfidentialClientApplicationBuilder |
| 257 | + .Create(TestConstants.ClientId) |
| 258 | + .WithAuthority(TestConstants.AuthorityCommonTenant) |
| 259 | + .WithHttpClientFactory(mockHttpClient) |
| 260 | + .WithInstanceDiscovery(false) |
| 261 | + .WithClientSecret(TestConstants.ClientSecret) |
| 262 | + .Build(); |
| 263 | + |
| 264 | + await msalMemoryTokenCacheProvider.InitializeAsync(confidentialApp.AppTokenCache).ConfigureAwait(false); |
| 265 | + |
| 266 | + AuthenticationResult result = await confidentialApp |
| 267 | + .AcquireTokenForClient(["https://graph.microsoft.com/.default"]) |
| 268 | + .ExecuteAsync() |
| 269 | + .ConfigureAwait(false); |
| 270 | + |
| 271 | + Assert.Equal( |
| 272 | + expectCacheHit ? |
| 273 | + TokenSource.Cache : |
| 274 | + TokenSource.IdentityProvider, |
| 275 | + result.AuthenticationResultMetadata.TokenSource); |
| 276 | + |
| 277 | + return result; |
| 278 | + |
| 279 | + } |
| 280 | + finally |
| 281 | + { |
| 282 | + handler?.Dispose(); |
| 283 | + mockHttpClient?.Dispose(); |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + #endregion |
| 288 | + |
| 289 | + [Fact] |
| 290 | + public async Task CacheKeyExtensibility() |
| 291 | + { |
| 292 | + var result = await GetTokensAssociatedWithKey("foo", expectCacheHit: false).ConfigureAwait(false); |
| 293 | + result = await GetTokensAssociatedWithKey("bar", expectCacheHit: false).ConfigureAwait(false); |
| 294 | + result = await GetTokensAssociatedWithKey(null, expectCacheHit: false).ConfigureAwait(false); |
| 295 | + |
| 296 | + result = await GetTokensAssociatedWithKey("foo", expectCacheHit: true).ConfigureAwait(false); |
| 297 | + result = await GetTokensAssociatedWithKey("bar", expectCacheHit: true).ConfigureAwait(false); |
| 298 | + result = await GetTokensAssociatedWithKey(null, expectCacheHit: true).ConfigureAwait(false); |
| 299 | + } |
| 300 | + |
193 | 301 | private enum CacheType |
194 | 302 | { |
195 | 303 | InMemory, |
|
0 commit comments