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
37 changes: 37 additions & 0 deletions src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.Identity.ServiceEssentials;
Comment thread
pmaytak marked this conversation as resolved.

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

/// <summary>
///
/// </summary>
/// <param name="identityCache"></param>
public CacheOptions(IIdentityCache identityCache)
{
IdentityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
}

/// <summary>
///
/// </summary>
/// <param name="appTokenCacheSizeLimit"></param>
/// <param name="userTokenCacheSizeLimit"></param>
public CacheOptions(int appTokenCacheSizeLimit, int userTokenCacheSizeLimit)
{
AppTokenCacheSizeLimit = appTokenCacheSizeLimit > 0 ? appTokenCacheSizeLimit : 0;
UserTokenCacheSizeLimit = userTokenCacheSizeLimit > 0 ? userTokenCacheSizeLimit : 0;
}

/// <summary>
/// Share the cache between all ClientApplication objects. The cache becomes static. Defaults to false.
/// </summary>
Expand All @@ -50,5 +73,19 @@ public CacheOptions(bool useSharedCache)
/// </remarks>
public bool UseSharedCache { get; set; }
Comment thread
pmaytak marked this conversation as resolved.

/// <summary>
/// User-provided instance of <c>IIdentityCache</c>
/// </summary>
public IIdentityCache IdentityCache { get; }

/// <summary>
/// Max count of cache items (by tenant for client credential flows) in the default in-memory cache with eviction
/// </summary>
public int AppTokenCacheSizeLimit { get; }

/// <summary>
/// Max count of cache items (by incoming token assertion cache for OBO and home account ID for other user flows) in the default in-memory cache with eviction
/// </summary>
public int UserTokenCacheSizeLimit { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task<Account> GetAccountAssociatedWithAccessTokenAsync(MsalAccessTo
public async Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem accessTokenCacheItem)
{
await RefreshCacheForReadOperationsAsync().ConfigureAwait(false);
return TokenCacheInternal.GetIdTokenCacheItem(accessTokenCacheItem);
return await TokenCacheInternal.GetIdTokenCacheItemAsync(accessTokenCacheItem).ConfigureAwait(false);
}

public async Task<MsalRefreshTokenCacheItem> FindFamilyRefreshTokenAsync(string familyId)
Expand Down Expand Up @@ -118,7 +118,7 @@ private async Task RefreshCacheForReadOperationsAsync()
var args = new TokenCacheNotificationArgs(
TokenCacheInternal,
_requestParams.AppConfig.ClientId,
_requestParams.Account,
_requestParams.Account,
hasStateChanged: false,
isApplicationCache: TokenCacheInternal.IsApplicationCache,
suggestedCacheKey: key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Cache.Keys;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.ServiceEssentials;

namespace Microsoft.Identity.Client.Cache
{
internal interface ITokenCacheAccessor
internal interface ITokenCacheAccessor : ICacheObject
{
void SaveAccessToken(MsalAccessTokenCacheItem item);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if DO_NOT_COMPILE
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Identity.ServiceEssentials;

namespace Microsoft.Identity.Client.Cache.Prototype
{
internal class DefaultInMemoryCache : IIdentityCache
{
private readonly MemoryCache _memoryCache;

public DefaultInMemoryCache(CacheOptions cacheOptions)
{
_memoryCache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = cacheOptions?.SizeLimit ?? 1000 });
}

public Task<CacheEntry<T>> GetAsync<T>(string category, string key, CancellationToken cancellationToken = default)
where T : ICacheObject
{
CacheEntry<T> result = null;
_memoryCache?.TryGetValue(key, out result);
return Task.FromResult(result);
}

public Task<CacheEntry<T>> SetAsync<T>(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default)
where T : ICacheObject
{
var cacheEntry = new CacheEntry<T>(value, DateTimeOffset.UtcNow.Add(cacheEntryOptions.ExpirationTimeRelativeToNow), DateTimeOffset.UtcNow.Add(cacheEntryOptions.RefreshTimeRelativeToNow));
var memoryCacheOptions = new MemoryCacheEntryOptions()
{
AbsoluteExpiration = cacheEntry.ExpirationTimeUTC,
Size = 1
};
return Task.FromResult(_memoryCache.Set(key, cacheEntry, memoryCacheOptions));
}

#region Not Implemented
public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public Task<CacheEntry<string>> GetAsync(string category, string key, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<CacheEntry<string>> SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
#endregion
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Identity.ServiceEssentials;
using Microsoft.Identity.ServiceEssentials.IdentityCache;
using Microsoft.IdentityModel.Abstractions;
Comment thread
pmaytak marked this conversation as resolved.

namespace Microsoft.Identity.Client.Cache.Prototype
{
internal class IdentityCacheWrapper
{
private static CacheOptions s_cacheOptions;
private readonly IIdentityCache _identityCache;
private static IIdentityLogger _identityLogger;
private static readonly Lazy<IIdentityCache> s_defaultIIdentityCache = new Lazy<IIdentityCache>(
() => CreateDefaultCache());
private const string AppTokensCategory = "app_tokens";
private const string UserTokensCategory = "user_tokens";

// This cache instance (whether provided by the user or default one) will only ever be called/used if cache serialization is not enabled.
// There are three options for this cache: user-provided, static default, non-static default.
// User-provided cache takes precedence.
// Default cache is created lazily (since it's possible that token cache serialization is enabled)
internal IdentityCacheWrapper(CacheOptions cacheOptions, IIdentityLogger identityLogger)
{
s_cacheOptions = cacheOptions;
_identityLogger = identityLogger;
// Set (or overwrite) cache to user-specified implementation, otherwise set to default implementation, if not already set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about default implentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, definitely it's adding a dependency to .NET Memory Cache. Added it for simplicity to show that it could work E2E. Practically we could replace it with some other memory cache with eviction that doesn't have dependencies (eg MiseCache).


if (cacheOptions.IdentityCache != null)
{
_identityCache = cacheOptions?.IdentityCache;
}
else if (cacheOptions.UseSharedCache)
{
_identityCache = s_defaultIIdentityCache.Value;
}
else
{
_identityCache = CreateDefaultCache();
}
}

private static IIdentityCache CreateDefaultCache()
{
var memoryCacheOptions = new InMemoryCacheOptions()
{
MaxNumberOfItemsForCategory = new Dictionary<string, int>()
{
{ AppTokensCategory, s_cacheOptions.AppTokenCacheSizeLimit },
{ UserTokensCategory, s_cacheOptions.UserTokenCacheSizeLimit },
}
};

return new IdentityCachePrototype(memoryCacheOptions, _identityLogger, null);
}

internal async Task<T> GetAppCacheAsync<T>(string key) where T : ICacheObject, new()
{
return await GetAsync<T>(AppTokensCategory, key).ConfigureAwait(false);
}

internal async Task<T> GetUserCacheAsync<T>(string key) where T : ICacheObject, new()
{
return await GetAsync<T>(UserTokensCategory, key).ConfigureAwait(false);
}

private async Task<T> GetAsync<T>(string category, string key) where T : ICacheObject, new()
{
var entry = await _identityCache.GetAsync<T>(category, key).ConfigureAwait(false);
return entry == null ? default : entry.Value;
}

internal async Task SetAppCacheAsync<T>(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new()
{
await SetAsync<T>(AppTokensCategory, key, value, cacheExpiry).ConfigureAwait(false);
}

internal async Task SetUserCacheAsync<T>(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new()
{
await SetAsync<T>(UserTokensCategory, key, value, cacheExpiry).ConfigureAwait(false);
}

private async Task SetAsync<T>(string category, string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new()
{
TimeSpan expirationTimeRelativeToNow = cacheExpiry.HasValue ? cacheExpiry.Value - DateTimeOffset.UtcNow : TimeSpan.FromHours(1);
await _identityCache.SetAsync(category, key, value, new CacheEntryOptions(expirationTimeRelativeToNow, 1)).ConfigureAwait(false);
}
}
}
15 changes: 12 additions & 3 deletions src/client/Microsoft.Identity.Client/ClientApplicationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Cache;
using Microsoft.Identity.Client.Cache.CacheImpl;
using Microsoft.Identity.Client.Cache.Prototype;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
using Microsoft.Identity.Client.Utils;
using static Microsoft.Identity.Client.TelemetryCore.Internal.Events.ApiEvent;

namespace Microsoft.Identity.Client
{
/// <summary>
Expand Down Expand Up @@ -62,18 +63,26 @@ public abstract partial class ClientApplicationBase : IClientApplicationBase

internal ITokenCacheInternal UserTokenCacheInternal { get; }

/// <summary>
/// TokenCache instance for implementation of IIdentityCache
/// </summary>
internal IdentityCacheWrapper IdentityCacheWrapper { get; }

internal ClientApplicationBase(ApplicationConfiguration config)
{
ServiceBundle = Internal.ServiceBundle.Create(config);
ICacheSerializationProvider defaultCacheSerialization = ServiceBundle.PlatformProxy.CreateTokenCacheBlobStorage();

// For this prototype, legacy cache serialization is disregarded, use user-provided or default IIdentityCacheImplementation.
IdentityCacheWrapper = new IdentityCacheWrapper(config.AccessorOptions ?? new CacheOptions(), ServiceBundle.Config.IdentityLogger);

if (config.UserTokenLegacyCachePersistenceForTest != null)
{
UserTokenCacheInternal = new TokenCache(ServiceBundle, config.UserTokenLegacyCachePersistenceForTest, false, defaultCacheSerialization);
UserTokenCacheInternal = new TokenCache(ServiceBundle, config.UserTokenLegacyCachePersistenceForTest, false, defaultCacheSerialization, identityCacheWrapper: IdentityCacheWrapper);
}
else
{
UserTokenCacheInternal = config.UserTokenCacheInternalForTest ?? new TokenCache(ServiceBundle, false, defaultCacheSerialization);
UserTokenCacheInternal = config.UserTokenCacheInternalForTest ?? new TokenCache(ServiceBundle, false, defaultCacheSerialization, identityCacheWrapper: IdentityCacheWrapper);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal ConfidentialClientApplication(
{
GuardMobileFrameworks();

AppTokenCacheInternal = configuration.AppTokenCacheInternalForTest ?? new TokenCache(ServiceBundle, true);
AppTokenCacheInternal = configuration.AppTokenCacheInternalForTest ?? new TokenCache(ServiceBundle, true, identityCacheWrapper: IdentityCacheWrapper);
Certificate = configuration.ClientCredentialCertificate;

this.ServiceBundle.ApplicationLogger.Verbose($"ConfidentialClientApplication {configuration.GetHashCode()} created");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> SaveTokenRe
MsalTokenResponse response);

Task<MsalAccessTokenCacheItem> FindAccessTokenAsync(AuthenticationRequestParameters requestParams);
MsalIdTokenCacheItem GetIdTokenCacheItem(MsalAccessTokenCacheItem msalAccessTokenCacheItem);
Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem msalAccessTokenCacheItem);

/// <summary>
/// Returns a RT for the request. If familyId is specified, it tries to return the FRT.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

<!-- Mobile and legacy targets - comment these out or set env variable MSAL_DESKTOP_ONLY_DEV to speedup the build -->
<PropertyGroup Condition="'$(MSAL_DESKTOP_ONLY_DEV)' == ''">
<TargetFrameworkNetDesktop45>net45</TargetFrameworkNetDesktop45>
<!--<TargetFrameworkNetDesktop45>net45</TargetFrameworkNetDesktop45>
<TargetFrameworkUap>uap10.0.17763</TargetFrameworkUap>
<TargetFrameworkIos>Xamarin.iOS10</TargetFrameworkIos>
<TargetFrameworkAndroid9>MonoAndroid9.0</TargetFrameworkAndroid9>
<TargetFrameworkAndroid10>MonoAndroid10.0</TargetFrameworkAndroid10>
<TargetFrameworkAndroid10>MonoAndroid10.0</TargetFrameworkAndroid10>-->
</PropertyGroup>

<!-- MAUI -->
Expand Down Expand Up @@ -46,7 +46,6 @@
<MsalClientSemVer Condition="'$(MsalClientSemVer)' == ''">4.46.0</MsalClientSemVer>
<!--This will generate AssemblyVersion, AssemblyFileVersion and AssemblyInformationVersion-->
<Version>$(MsalClientSemVer)</Version>

<XamarinAndroidSupportSkipVerifyVersions>true</XamarinAndroidSupportSkipVerifyVersions>

<!-- Copyright needs to be in the form of © not (c) to be compliant -->
Expand Down Expand Up @@ -306,6 +305,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="6.22.0" />
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="6.23.1" />
<PackageReference Include="Microsoft.Identity.ServiceEssentials.Abstractions" Version="1.9.2" />
<PackageReference Include="Microsoft.Identity.ServiceEssentials.IdentityCache" Version="1.9.2" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Identity.Client.Cache.Keys;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.ServiceEssentials;

namespace Microsoft.Identity.Client.PlatformsCommon.Shared
{
Expand All @@ -19,7 +20,7 @@ namespace Microsoft.Identity.Client.PlatformsCommon.Shared
/// App metadata collection is not partitioned.
/// Refresh token, ID token, and account related methods are no-op.
/// </summary>
internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor
internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor, ICacheObject
{
// perf: do not use ConcurrentDictionary.Values as it takes a lock
// internal for test only
Expand All @@ -35,6 +36,11 @@ internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor
protected readonly ILoggerAdapter _logger;
private readonly CacheOptions _tokenCacheAccessorOptions;

public InMemoryPartitionedAppTokenCacheAccessor()
{

}

public InMemoryPartitionedAppTokenCacheAccessor(
ILoggerAdapter logger,
CacheOptions tokenCacheAccessorOptions)
Expand Down Expand Up @@ -228,5 +234,15 @@ public virtual bool HasAccessOrRefreshTokens()
{
return AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer()));
}

public byte[] Serialize()
{
throw new NotImplementedException();
}

public void Deserialize(byte[] serializedValue)
{
throw new NotImplementedException();
}
}
}
Loading