Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Identity.Client
{
/// <summary>
Expand Down Expand Up @@ -39,6 +41,17 @@ public CacheOptions(bool useSharedCache)
UseSharedCache = useSharedCache;
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="useSharedCache">Set to true to share the cache between all ClientApplication objects. The cache becomes static. <see cref="UseSharedCache"/> for a detailed description. </param>
/// <param name="maximumItems">Token cache items limit. <see cref="MaximumItems"/> for a detailed description.</param>
public CacheOptions(bool useSharedCache, int maximumItems)
{
UseSharedCache = useSharedCache;
MaximumItems = maximumItems;
}

/// <summary>
/// Share the cache between all ClientApplication objects. The cache becomes static. Defaults to false.
/// </summary>
Expand All @@ -50,5 +63,34 @@ public CacheOptions(bool useSharedCache)
/// </remarks>
public bool UseSharedCache { get; set; }

private int? _maximumItems;

/// <summary>
/// Total token cache items limit for both app and user token caches.
/// </summary>
/// <remarks>
/// Once the limit is reached, either the app or user cache will be compacted, depending on which was most recently used.
/// Using a MemoryCache via Microsoft.Identity.Web.TokenCache is more accurate but slower.
/// This size limit applies only to internal memory cache and is not a concern when distributed caching is used.
/// IMPORTANT: Monitor app health metrics (including memory usage) and cache performance (<see href="https://aka.ms/msal-net-token-cache-serialization"/>)
/// and adjust size limit accordingly.
/// </remarks>
public int? MaximumItems
{
get => _maximumItems;
set
{
ValidateSizeLimit(value);
_maximumItems = value;
}
}

private void ValidateSizeLimit(int? maximumItems)
{
if (maximumItems < 0)
{
throw new ArgumentOutOfRangeException(nameof(maximumItems), $"{nameof(maximumItems)} must be a positive number.");
}
}
}
}
603 changes: 603 additions & 0 deletions src/client/Microsoft.Identity.Client/Cache/EventBasedLRUCache.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal interface ITokenCacheAccessor
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string optionalPartitionKey = null);
IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens();

/// <summary>
/// Returns all refresh tokens from the underlying cache collection.
Expand All @@ -53,7 +53,7 @@ internal interface ITokenCacheAccessor
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(string optionalPartitionKey = null);
IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens();

/// <summary>
/// Returns all ID tokens from the underlying cache collection.
Expand All @@ -64,7 +64,7 @@ internal interface ITokenCacheAccessor
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string optionalPartitionKey = null);
IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens();

/// <summary>
/// Returns all accounts from the underlying cache collection.
Expand All @@ -75,7 +75,7 @@ internal interface ITokenCacheAccessor
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccountCacheItem> GetAllAccounts(string optionalPartitionKey = null);
IReadOnlyList<MsalAccountCacheItem> GetAllAccounts();

IReadOnlyList<MsalAppMetadataCacheItem> GetAllAppMetadata();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor
{
// perf: do not use ConcurrentDictionary.Values as it takes a lock
// internal for test only
internal readonly ConcurrentDictionary<string, ConcurrentDictionary<string, MsalAccessTokenCacheItem>> AccessTokenCacheDictionary;
internal readonly ConcurrentDictionary<string, MsalAccessTokenCacheItem> AccessTokenCacheDictionary;
internal readonly ConcurrentDictionary<string, MsalAppMetadataCacheItem> AppMetadataDictionary;

// static versions to support the "shared cache" mode
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, MsalAccessTokenCacheItem>> s_accessTokenCacheDictionary =
new ConcurrentDictionary<string, ConcurrentDictionary<string, MsalAccessTokenCacheItem>>();
private static readonly ConcurrentDictionary<string, MsalAccessTokenCacheItem> s_accessTokenCacheDictionary =
new ConcurrentDictionary<string, MsalAccessTokenCacheItem>();
private static readonly ConcurrentDictionary<string, MsalAppMetadataCacheItem> s_appMetadataDictionary =
new ConcurrentDictionary<string, MsalAppMetadataCacheItem>(1, 1);

Expand All @@ -49,7 +49,7 @@ public InMemoryPartitionedAppTokenCacheAccessor(
}
else
{
AccessTokenCacheDictionary = new ConcurrentDictionary<string, ConcurrentDictionary<string, MsalAccessTokenCacheItem>>();
AccessTokenCacheDictionary = new ConcurrentDictionary<string, MsalAccessTokenCacheItem>();
AppMetadataDictionary = new ConcurrentDictionary<string, MsalAppMetadataCacheItem>();
}
}
Expand All @@ -58,11 +58,9 @@ public InMemoryPartitionedAppTokenCacheAccessor(
public void SaveAccessToken(MsalAccessTokenCacheItem item)
{
string itemKey = item.GetKey().ToString();
string partitionKey = CacheKeyFactory.GetClientCredentialKey(item.ClientId, item.TenantId, item.KeyId);

// if a conflict occurs, pick the latest value
AccessTokenCacheDictionary
.GetOrAdd(partitionKey, new ConcurrentDictionary<string, MsalAccessTokenCacheItem>())[itemKey] = item;
AccessTokenCacheDictionary[itemKey] = item;
}

/// <summary>
Expand Down Expand Up @@ -128,10 +126,7 @@ public MsalAppMetadataCacheItem GetAppMetadata(MsalAppMetadataCacheKey appMetada
#region Delete
public void DeleteAccessToken(MsalAccessTokenCacheItem item)
{
var partitionKey = CacheKeyFactory.GetClientCredentialKey(item.ClientId, item.TenantId, item.KeyId);

AccessTokenCacheDictionary.TryGetValue(partitionKey, out var partition);
if (partition == null || !partition.TryRemove(item.GetKey().ToString(), out _))
if (!AccessTokenCacheDictionary.TryRemove(item.GetKey().ToString(), out _))
{
_logger.InfoPii(
$"Cannot delete access token because it was not found in the cache. Key {item.GetKey()}.",
Expand Down Expand Up @@ -173,31 +168,24 @@ public void DeleteAccount(MsalAccountCacheItem item)
/// WARNING: if partitonKey = null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// </summary>
public virtual IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string partitionKey = null)
public virtual IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens()
{
_logger.Always($"[GetAllAccessTokens] Total number of cache partitions found while getting access tokens: {AccessTokenCacheDictionary.Count}");
if (string.IsNullOrEmpty(partitionKey))
{
return AccessTokenCacheDictionary.SelectMany(dict => dict.Value).Select(kv => kv.Value).ToList();
}
else
{
AccessTokenCacheDictionary.TryGetValue(partitionKey, out ConcurrentDictionary<string, MsalAccessTokenCacheItem> partition);
return partition?.Select(kv => kv.Value)?.ToList() ?? CollectionHelpers.GetEmptyReadOnlyList<MsalAccessTokenCacheItem>();
}
_logger.Always($"[GetAllAccessTokens] Total number of items found while getting access tokens: {AccessTokenCacheDictionary.Count}");
return AccessTokenCacheDictionary.Select(kv => kv.Value).ToList();

}

public virtual IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(string partitionKey = null)
public virtual IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens()
{
return CollectionHelpers.GetEmptyReadOnlyList<MsalRefreshTokenCacheItem>();
}

public virtual IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string partitionKey = null)
public virtual IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens()
{
return CollectionHelpers.GetEmptyReadOnlyList<MsalIdTokenCacheItem>();
}

public virtual IReadOnlyList<MsalAccountCacheItem> GetAllAccounts(string partitionKey = null)
public virtual IReadOnlyList<MsalAccountCacheItem> GetAllAccounts()
{
return CollectionHelpers.GetEmptyReadOnlyList<MsalAccountCacheItem>();
}
Expand All @@ -222,7 +210,7 @@ public virtual void Clear()

public virtual bool HasAccessOrRefreshTokens()
{
return AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer()));
return AccessTokenCacheDictionary.Any(token => !token.Value.IsExpiredWithBuffer());
}
}
}
Loading