diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs index 6b5d873327..2fcbc6780d 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using Microsoft.Identity.ServiceEssentials; + namespace Microsoft.Identity.Client { /// @@ -39,6 +42,26 @@ public CacheOptions(bool useSharedCache) UseSharedCache = useSharedCache; } + /// + /// + /// + /// + public CacheOptions(IIdentityCache identityCache) + { + IdentityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); + } + + /// + /// + /// + /// + /// + public CacheOptions(int appTokenCacheSizeLimit, int userTokenCacheSizeLimit) + { + AppTokenCacheSizeLimit = appTokenCacheSizeLimit > 0 ? appTokenCacheSizeLimit : 0; + UserTokenCacheSizeLimit = userTokenCacheSizeLimit > 0 ? userTokenCacheSizeLimit : 0; + } + /// /// Share the cache between all ClientApplication objects. The cache becomes static. Defaults to false. /// @@ -50,5 +73,19 @@ public CacheOptions(bool useSharedCache) /// public bool UseSharedCache { get; set; } + /// + /// User-provided instance of IIdentityCache + /// + public IIdentityCache IdentityCache { get; } + + /// + /// Max count of cache items (by tenant for client credential flows) in the default in-memory cache with eviction + /// + public int AppTokenCacheSizeLimit { get; } + + /// + /// 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 + /// + public int UserTokenCacheSizeLimit { get; } } } diff --git a/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs b/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs index 0e744cfe48..caa3544bbc 100644 --- a/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs +++ b/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs @@ -58,7 +58,7 @@ public async Task GetAccountAssociatedWithAccessTokenAsync(MsalAccessTo public async Task GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem accessTokenCacheItem) { await RefreshCacheForReadOperationsAsync().ConfigureAwait(false); - return TokenCacheInternal.GetIdTokenCacheItem(accessTokenCacheItem); + return await TokenCacheInternal.GetIdTokenCacheItemAsync(accessTokenCacheItem).ConfigureAwait(false); } public async Task FindFamilyRefreshTokenAsync(string familyId) @@ -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, diff --git a/src/client/Microsoft.Identity.Client/Cache/ITokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/Cache/ITokenCacheAccessor.cs index 557a18877f..a5e7cb6a9d 100644 --- a/src/client/Microsoft.Identity.Client/Cache/ITokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/Cache/ITokenCacheAccessor.cs @@ -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); diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs new file mode 100644 index 0000000000..f68e7999b9 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs @@ -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> GetAsync(string category, string key, CancellationToken cancellationToken = default) + where T : ICacheObject + { + CacheEntry result = null; + _memoryCache?.TryGetValue(key, out result); + return Task.FromResult(result); + } + + public Task> SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + where T : ICacheObject + { + var cacheEntry = new CacheEntry(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> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +#endregion + } +} +#endif diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs new file mode 100644 index 0000000000..42b46dc4b3 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -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; + +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 s_defaultIIdentityCache = new Lazy( + () => 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. + + 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() + { + { AppTokensCategory, s_cacheOptions.AppTokenCacheSizeLimit }, + { UserTokensCategory, s_cacheOptions.UserTokenCacheSizeLimit }, + } + }; + + return new IdentityCachePrototype(memoryCacheOptions, _identityLogger, null); + } + + internal async Task GetAppCacheAsync(string key) where T : ICacheObject, new() + { + return await GetAsync(AppTokensCategory, key).ConfigureAwait(false); + } + + internal async Task GetUserCacheAsync(string key) where T : ICacheObject, new() + { + return await GetAsync(UserTokensCategory, key).ConfigureAwait(false); + } + + private async Task GetAsync(string category, string key) where T : ICacheObject, new() + { + var entry = await _identityCache.GetAsync(category, key).ConfigureAwait(false); + return entry == null ? default : entry.Value; + } + + internal async Task SetAppCacheAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new() + { + await SetAsync(AppTokensCategory, key, value, cacheExpiry).ConfigureAwait(false); + } + + internal async Task SetUserCacheAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new() + { + await SetAsync(UserTokensCategory, key, value, cacheExpiry).ConfigureAwait(false); + } + + private async Task SetAsync(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); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs index 9b6c560ea3..d440168d53 100644 --- a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs +++ b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs @@ -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 { /// @@ -62,18 +63,26 @@ public abstract partial class ClientApplicationBase : IClientApplicationBase internal ITokenCacheInternal UserTokenCacheInternal { get; } + /// + /// TokenCache instance for implementation of IIdentityCache + /// + 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); } } diff --git a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs index 6f232e5258..e9444c7e1e 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -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"); diff --git a/src/client/Microsoft.Identity.Client/ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/ITokenCacheInternal.cs index e8937f5d7d..ee6793eabf 100644 --- a/src/client/Microsoft.Identity.Client/ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/ITokenCacheInternal.cs @@ -27,7 +27,7 @@ Task> SaveTokenRe MsalTokenResponse response); Task FindAccessTokenAsync(AuthenticationRequestParameters requestParams); - MsalIdTokenCacheItem GetIdTokenCacheItem(MsalAccessTokenCacheItem msalAccessTokenCacheItem); + Task GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem msalAccessTokenCacheItem); /// /// Returns a RT for the request. If familyId is specified, it tries to return the FRT. diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index a81752c6cd..53f2e6ee66 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -12,11 +12,11 @@ - net45 + @@ -46,7 +46,6 @@ 4.46.0 $(MsalClientSemVer) - true @@ -306,6 +305,8 @@ - + + + diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs index d9b72ba444..9ee802741d 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs @@ -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 { @@ -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. /// - internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor + internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor, ICacheObject { // perf: do not use ConcurrentDictionary.Values as it takes a lock // internal for test only @@ -35,6 +36,11 @@ internal class InMemoryPartitionedAppTokenCacheAccessor : ITokenCacheAccessor protected readonly ILoggerAdapter _logger; private readonly CacheOptions _tokenCacheAccessorOptions; + public InMemoryPartitionedAppTokenCacheAccessor() + { + + } + public InMemoryPartitionedAppTokenCacheAccessor( ILoggerAdapter logger, CacheOptions tokenCacheAccessorOptions) @@ -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(); + } } } diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs index 3b2bfa8ff5..2728457247 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs @@ -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 { @@ -19,7 +20,7 @@ namespace Microsoft.Identity.Client.PlatformsCommon.Shared /// Partitions the ID token and account collections by home account ID. /// App metadata collection is not partitioned. /// - internal class InMemoryPartitionedUserTokenCacheAccessor : ITokenCacheAccessor + internal class InMemoryPartitionedUserTokenCacheAccessor : ITokenCacheAccessor, ICacheObject { // perf: do not use ConcurrentDictionary.Values as it takes a lock // internal for test only @@ -44,6 +45,11 @@ internal class InMemoryPartitionedUserTokenCacheAccessor : ITokenCacheAccessor protected readonly ILoggerAdapter _logger; private readonly CacheOptions _tokenCacheAccessorOptions; + public InMemoryPartitionedUserTokenCacheAccessor() + { + + } + public InMemoryPartitionedUserTokenCacheAccessor(ILoggerAdapter logger, CacheOptions tokenCacheAccessorOptions) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -295,5 +301,15 @@ public virtual bool HasAccessOrRefreshTokens() return RefreshTokenCacheDictionary.Any(partition => partition.Value.Count > 0) || AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer())); } + + public byte[] Serialize() + { + throw new NotImplementedException(); + } + + public void Deserialize(byte[] serializedValue) + { + throw new NotImplementedException(); + } } } diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index 5781b61bde..98633a567b 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -18,6 +18,8 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.PlatformsCommon.Factories; +using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; @@ -60,7 +62,7 @@ async Task> IToke // token could be comming from a different cloud than the one configured if (requestParams.AppConfig.MultiCloudSupportEnabled && !string.IsNullOrEmpty(response.AuthorityUrl)) { - var url = new Uri(response.AuthorityUrl); + var url = new Uri(response.AuthorityUrl); requestParams.AuthorityManager = new AuthorityManager( requestParams.RequestContext, Authority.CreateAuthorityWithEnvironment(requestParams.Authority.AuthorityInfo, url.Host)); @@ -186,6 +188,8 @@ async Task> IToke requestParams.RequestContext.ApiEvent.DurationInCacheInMs += sw.ElapsedMilliseconds; } + var accessor = await GetOrCreateAccessorAsync(suggestedWebCacheKey).ConfigureAwait(false); + // Don't cache PoP access tokens from broker if (msalAccessTokenCacheItem != null && !(response.TokenSource == TokenSource.Broker && response.TokenType == Constants.PoPAuthHeaderPrefix)) { @@ -196,9 +200,10 @@ async Task> IToke tenantId, msalAccessTokenCacheItem.ScopeSet, msalAccessTokenCacheItem.HomeAccountId, - msalAccessTokenCacheItem.TokenType); + msalAccessTokenCacheItem.TokenType, + accessor); - Accessor.SaveAccessToken(msalAccessTokenCacheItem); + accessor.SaveAccessToken(msalAccessTokenCacheItem); } if (idToken != null) @@ -206,20 +211,34 @@ async Task> IToke logger.Info("[SaveTokenResponseAsync] Saving Id Token and Account in cache ..."); Accessor.SaveIdToken(msalIdTokenCacheItem); MergeWamAccountIds(msalAccountCacheItem); - Accessor.SaveAccount(msalAccountCacheItem); + accessor.SaveAccount(msalAccountCacheItem); } // if server returns the refresh token back, save it in the cache. if (msalRefreshTokenCacheItem != null) { logger.Info("[SaveTokenResponseAsync] Saving RT in cache..."); - Accessor.SaveRefreshToken(msalRefreshTokenCacheItem); + accessor.SaveRefreshToken(msalRefreshTokenCacheItem); } UpdateAppMetadata( requestParams.AppConfig.ClientId, instanceDiscoveryMetadata.PreferredCache, - response.FamilyId); + response.FamilyId, + accessor); + + if (!((ITokenCacheInternal)this).IsAppSubscribedToSerializationEvents()) + { + DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, logger); + if (accessor is InMemoryPartitionedAppTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAppCacheAsync(suggestedWebCacheKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) + { + await IdentityCacheWrapper.SetUserCacheAsync(suggestedWebCacheKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + } SaveToLegacyAdalCache( requestParams, @@ -276,7 +295,6 @@ async Task> IToke //This will run on a background thread to mitigate this. private void DumpCacheToLogs(AuthenticationRequestParameters requestParameters) { - if (requestParameters.RequestContext.Logger.IsLoggingEnabled(LogLevel.Verbose)) { var accessTokenCacheItems = Accessor.GetAllAccessTokens(); @@ -385,7 +403,9 @@ private void SaveToLegacyAdalCache( // do not suggest an expiration date from the past or within 5 min, as tokens will not be usable anyway // and HasTokens will be set to false, letting implementers know to delete the cache node if (cacheExpiry < DateTimeOffset.UtcNow + Constants.AccessTokenExpirationBuffer) + { return null; + } return cacheExpiry; } @@ -425,7 +445,7 @@ async Task ITokenCacheInternal.FindAccessTokenAsync( string partitionKey = CacheKeyFactory.GetKeyFromRequest(requestParams); Debug.Assert(partitionKey != null || !requestParams.IsConfidentialClient, "On confidential client, cache must be partitioned."); - var accessTokens = Accessor.GetAllAccessTokens(partitionKey, logger); + var accessTokens = (await GetOrCreateAccessorAsync(partitionKey).ConfigureAwait(false)).GetAllAccessTokens(partitionKey, logger); requestParams.RequestContext.Logger.Always($"[FindAccessTokenAsync] Discovered {accessTokens.Count} access tokens in cache using partition key: {partitionKey}"); @@ -752,10 +772,12 @@ async Task ITokenCacheInternal.FindRefreshTokenAsync( string familyId) { if (requestParams.Authority == null) + { return null; + } var requestKey = CacheKeyFactory.GetKeyFromRequest(requestParams); - var refreshTokens = Accessor.GetAllRefreshTokens(requestKey); + var refreshTokens = (await GetOrCreateAccessorAsync(requestKey).ConfigureAwait(false)).GetAllRefreshTokens(requestKey); requestParams.RequestContext.Logger.Always($"[FindRefreshTokenAsync] Discovered {refreshTokens.Count} refresh tokens in cache using key: {requestKey}"); if (refreshTokens.Count != 0) @@ -913,7 +935,9 @@ async Task> ITokenCacheInternal.GetAccountsAsync(Authentic } if (logger.IsLoggingEnabled(LogLevel.Verbose)) + { logger.Verbose($"[GetAccounts] Found {refreshTokenCacheItems.Count} RTs and {accountCacheItems.Count} accounts in MSAL cache. "); + } // Multi-cloud support - must filter by environment. ISet allEnvironmentsInCache = new HashSet( @@ -946,7 +970,9 @@ async Task> ITokenCacheInternal.GetAccountsAsync(Authentic } if (logger.IsLoggingEnabled(LogLevel.Verbose)) + { logger.Verbose($"[GetAccounts] Found {refreshTokenCacheItems.Count} RTs and {accountCacheItems.Count} accounts in MSAL cache after environment filtering. "); + } IDictionary clientInfoToAccountMap = new Dictionary(); foreach (MsalRefreshTokenCacheItem rtItem in refreshTokenCacheItems) @@ -1020,7 +1046,9 @@ async Task> ITokenCacheInternal.GetAccountsAsync(Authentic StringComparison.OrdinalIgnoreCase)).ToList(); if (logger.IsLoggingEnabled(LogLevel.Verbose)) + { logger.Verbose($"Filtered by home account id. Remaining accounts {accounts.Count()} "); + } } return accounts; @@ -1063,9 +1091,9 @@ private void UpdateWithAdalAccountsWithoutClientInfo( } } - MsalIdTokenCacheItem ITokenCacheInternal.GetIdTokenCacheItem(MsalAccessTokenCacheItem msalAccessTokenCacheItem) + async Task ITokenCacheInternal.GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem msalAccessTokenCacheItem) { - var idToken = Accessor.GetIdToken(msalAccessTokenCacheItem); + var idToken = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetIdToken(msalAccessTokenCacheItem); return idToken; } @@ -1080,7 +1108,7 @@ private async Task> GetTenantProfilesAsync( Debug.Assert(homeAccountId != null); - var idTokenCacheItems = Accessor.GetAllIdTokens(homeAccountId); + var idTokenCacheItems = (await GetOrCreateAccessorAsync(homeAccountId).ConfigureAwait(false)).GetAllIdTokens(homeAccountId); FilterTokensByClientId(idTokenCacheItems); if (!requestParameters.AppConfig.MultiCloudSupportEnabled) @@ -1117,7 +1145,7 @@ async Task ITokenCacheInternal.GetAccountAssociatedWithAccessTokenAsync var tenantProfiles = await GetTenantProfilesAsync(requestParameters, msalAccessTokenCacheItem.HomeAccountId).ConfigureAwait(false); - var accountCacheItem = Accessor.GetAccount( + var accountCacheItem = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetAccount( new MsalAccountCacheKey( msalAccessTokenCacheItem.Environment, msalAccessTokenCacheItem.TenantId, @@ -1164,12 +1192,11 @@ async Task ITokenCacheInternal.RemoveAccountAsync(IAccount account, Authenticati identityLogger: requestParameters.RequestContext.Logger.IdentityLogger, piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); - await tokenCacheInternal.OnBeforeAccessAsync(args).ConfigureAwait(false); await tokenCacheInternal.OnBeforeWriteAsync(args).ConfigureAwait(false); } - RemoveAccountInternal(account, requestParameters.RequestContext); + RemoveAccountInternalAsync(account, requestParameters.RequestContext); if (IsLegacyAdalCacheEnabled(requestParameters)) { CacheFallbackOperations.RemoveAdalUser( @@ -1200,7 +1227,6 @@ async Task ITokenCacheInternal.RemoveAccountAsync(IAccount account, Authenticati identityLogger: requestParameters.RequestContext.Logger.IdentityLogger, piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); - await tokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); } } @@ -1220,7 +1246,7 @@ bool ITokenCacheInternal.HasTokensNoLocks() return Accessor.HasAccessOrRefreshTokens(); } - internal /* internal for test only */ void RemoveAccountInternal(IAccount account, RequestContext requestContext) + internal /* internal for test only */ async Task RemoveAccountInternalAsync(IAccount account, RequestContext requestContext) { if (account.HomeAccountId == null) { @@ -1230,7 +1256,9 @@ bool ITokenCacheInternal.HasTokensNoLocks() string partitionKey = account.HomeAccountId.Identifier; - var refreshTokens = Accessor.GetAllRefreshTokens(partitionKey); + var accessor = await GetOrCreateAccessorAsync(partitionKey).ConfigureAwait(false); + + var refreshTokens = accessor.GetAllRefreshTokens(partitionKey); refreshTokens.RemoveAll(item => !item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase)); // To maintain backward compatibility with other MSALs, filter all credentials by clientID if @@ -1245,12 +1273,12 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalRefreshTokenCacheItem refreshTokenCacheItem in refreshTokens) { - Accessor.DeleteRefreshToken(refreshTokenCacheItem); + accessor.DeleteRefreshToken(refreshTokenCacheItem); } requestContext.Logger.Info($"[RemoveAccountAsync] Deleted {refreshTokens.Count} refresh tokens."); - var accessTokens = Accessor.GetAllAccessTokens(partitionKey); + var accessTokens = accessor.GetAllAccessTokens(partitionKey); accessTokens.RemoveAll(item => !item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase)); if (filterByClientId) { @@ -1259,12 +1287,12 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalAccessTokenCacheItem accessTokenCacheItem in accessTokens) { - Accessor.DeleteAccessToken(accessTokenCacheItem); + accessor.DeleteAccessToken(accessTokenCacheItem); } requestContext.Logger.Info($"[RemoveAccountAsync] Deleted {accessTokens.Count} access tokens."); - var idTokens = Accessor.GetAllIdTokens(partitionKey); + var idTokens = accessor.GetAllIdTokens(partitionKey); idTokens.RemoveAll(item => !item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase)); if (filterByClientId) { @@ -1273,18 +1301,62 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalIdTokenCacheItem idTokenCacheItem in idTokens) { - Accessor.DeleteIdToken(idTokenCacheItem); + accessor.DeleteIdToken(idTokenCacheItem); } requestContext.Logger.Info($"[RemoveAccountAsync] Deleted {idTokens.Count} ID tokens."); - var accounts = Accessor.GetAllAccounts(partitionKey); + var accounts = accessor.GetAllAccounts(partitionKey); accounts.RemoveAll(item => !(item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase) && item.PreferredUsername.Equals(account.Username, StringComparison.OrdinalIgnoreCase))); foreach (MsalAccountCacheItem accountCacheItem in accounts) { - Accessor.DeleteAccount(accountCacheItem); + accessor.DeleteAccount(accountCacheItem); + } + + if (!((ITokenCacheInternal)this).IsAppSubscribedToSerializationEvents()) + { + DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, requestContext.Logger); + if (accessor is InMemoryPartitionedAppTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAppCacheAsync(partitionKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) + { + await IdentityCacheWrapper.SetUserCacheAsync(partitionKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + } + } + + // Cache setup is validated to be mutually exclusive - + // Token cache serialization is allowed only when WithCacheOptions is not used. + internal async Task GetOrCreateAccessorAsync(string partitionKey) + { + // If user set up legacy cache serialization, then use old accessor instance (it would have been populated with tokens) + // Otherwise, use IIdentityCache instance, either the user-provided or default. + if (((ITokenCacheInternal)this).IsAppSubscribedToSerializationEvents()) + { + return Accessor; + } + else + { + ITokenCacheAccessor cachedAccessor; + if (IsAppTokenCache) + { + cachedAccessor = await IdentityCacheWrapper.GetAppCacheAsync(partitionKey).ConfigureAwait(false); + } + else + { + cachedAccessor = await IdentityCacheWrapper.GetUserCacheAsync(partitionKey).ConfigureAwait(false); + } + + if (cachedAccessor == null) + { + var proxy = ServiceBundle?.PlatformProxy ?? PlatformProxyFactory.CreatePlatformProxy(null); + cachedAccessor = proxy.CreateTokenCacheAccessor(ServiceBundle.Config.AccessorOptions, IsAppTokenCache); + } + return cachedAccessor; } } } diff --git a/src/client/Microsoft.Identity.Client/TokenCache.cs b/src/client/Microsoft.Identity.Client/TokenCache.cs index d224edae69..9d61c49fab 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.cs @@ -10,6 +10,7 @@ using Microsoft.Identity.Client.Cache; using Microsoft.Identity.Client.Cache.CacheImpl; using Microsoft.Identity.Client.Cache.Items; +using Microsoft.Identity.Client.Cache.Prototype; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; @@ -39,6 +40,9 @@ public sealed partial class TokenCache : ITokenCacheInternal internal ITokenCacheAccessor Accessor { get; set; } + + internal IdentityCacheWrapper IdentityCacheWrapper { get; set; } + internal IServiceBundle ServiceBundle { get; } internal ILegacyCachePersistence LegacyCachePersistence { get; set; } @@ -69,7 +73,11 @@ public TokenCache() : this((IServiceBundle)null, false, null) { } - internal TokenCache(IServiceBundle serviceBundle, bool isApplicationTokenCache, ICacheSerializationProvider optionalDefaultSerializer = null) + internal TokenCache( + IServiceBundle serviceBundle, + bool isApplicationTokenCache, + ICacheSerializationProvider optionalDefaultSerializer = null, + IdentityCacheWrapper identityCacheWrapper = null) { if (serviceBundle == null) throw new ArgumentNullException(nameof(serviceBundle)); @@ -94,6 +102,8 @@ internal TokenCache(IServiceBundle serviceBundle, bool isApplicationTokenCache, // Must happen last, this code can access things like _accessor and such above. ServiceBundle = serviceBundle; + + IdentityCacheWrapper = identityCacheWrapper; } /// @@ -103,8 +113,9 @@ internal TokenCache( IServiceBundle serviceBundle, ILegacyCachePersistence legacyCachePersistenceForTest, bool isApplicationTokenCache, - ICacheSerializationProvider optionalDefaultCacheSerializer = null) - : this(serviceBundle, isApplicationTokenCache, optionalDefaultCacheSerializer) + ICacheSerializationProvider optionalDefaultCacheSerializer = null, + IdentityCacheWrapper identityCacheWrapper = null) + : this(serviceBundle, isApplicationTokenCache, optionalDefaultCacheSerializer, identityCacheWrapper) { LegacyCachePersistence = legacyCachePersistenceForTest; } @@ -118,12 +129,12 @@ public void SetIosKeychainSecurityGroup(string securityGroup) #endif } - private void UpdateAppMetadata(string clientId, string environment, string familyId) + private void UpdateAppMetadata(string clientId, string environment, string familyId, ITokenCacheAccessor accessor) { if (_featureFlags.IsFociEnabled) { var metadataCacheItem = new MsalAppMetadataCacheItem(clientId, environment, familyId); - Accessor.SaveAppMetadata(metadataCacheItem); + accessor.SaveAppMetadata(metadataCacheItem); } } @@ -138,7 +149,8 @@ private void DeleteAccessTokensWithIntersectingScopes( string tenantId, HashSet scopeSet, string homeAccountId, - string tokenType) + string tokenType, + ITokenCacheAccessor accessor = null) { if (requestParams.RequestContext.Logger.IsLoggingEnabled(LogLevel.Info)) { @@ -151,7 +163,7 @@ private void DeleteAccessTokensWithIntersectingScopes( var partitionKeyFromResponse = CacheKeyFactory.GetInternalPartitionKeyFromResponse(requestParams, homeAccountId); Debug.Assert(partitionKeyFromResponse != null || !requestParams.IsConfidentialClient, "On confidential client, cache must be partitioned."); - foreach (var accessToken in Accessor.GetAllAccessTokens(partitionKeyFromResponse)) + foreach (var accessToken in (accessor ?? Accessor).GetAllAccessTokens(partitionKeyFromResponse)) { if (accessToken.ClientId.Equals(ClientId, StringComparison.OrdinalIgnoreCase) && environmentAliases.Contains(accessToken.Environment) && diff --git a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs index 03a0b4afee..a9a6e0f6e2 100644 --- a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs @@ -8,6 +8,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Cache; using Microsoft.Identity.Client.Cache.Items; +using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.Identity.Test.Performance.Helpers; using Microsoft.Identity.Test.Unit; @@ -54,12 +55,18 @@ public class AcquireTokenForClientCacheTests [GlobalSetup] public async Task GlobalSetupAsync() { - _cca = ConfidentialClientApplicationBuilder + var builder = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithRedirectUri(TestConstants.RedirectUri) .WithClientSecret(TestConstants.ClientSecret) - .WithLegacyCacheCompatibility(false) - .BuildConcrete(); + .WithLegacyCacheCompatibility(false); + + if (!EnableCacheSerialization) + { + builder.WithCacheOptions(new CacheOptions(CacheSize.TotalTenants, 0)); + } + + _cca = builder.BuildConcrete(); if (EnableCacheSerialization) { @@ -102,6 +109,16 @@ private async Task PopulateAppCacheAsync(ConfidentialClientApplication cca, int { string key = CacheKeyFactory.GetClientCredentialKey(_cca.AppConfig.ClientId, $"{_tenantPrefix}{tenant}", ""); + ITokenCacheAccessor accessor; + if (enableCacheSerialization) + { + accessor = cca.AppTokenCacheInternal.Accessor; + } + else + { + accessor = await (cca.AppTokenCache as TokenCache).GetOrCreateAccessorAsync(key).ConfigureAwait(false); + } + for (int token = 0; token < tokensPerTenant; token++) { MsalAccessTokenCacheItem atItem = TokenCacheHelper.CreateAccessTokenItem( @@ -109,7 +126,7 @@ private async Task PopulateAppCacheAsync(ConfidentialClientApplication cca, int tenant: $"{_tenantPrefix}{tenant}", accessToken: TestConstants.AppAccessToken); - cca.AppTokenCacheInternal.Accessor.SaveAccessToken(atItem); + accessor.SaveAccessToken(atItem); } if (enableCacheSerialization) @@ -127,6 +144,10 @@ private async Task PopulateAppCacheAsync(ConfidentialClientApplication cca, int await cca.AppTokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); cca.AppTokenCacheInternal.Accessor.Clear(); } + else + { + await (cca.AppTokenCache as TokenCache).IdentityCacheWrapper.SetAppCacheAsync(key, (InMemoryPartitionedAppTokenCacheAccessor)accessor, null).ConfigureAwait(false); + } } } } diff --git a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs index c0a951ce91..a6092b04de 100644 --- a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs @@ -6,7 +6,9 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Cache; using Microsoft.Identity.Client.Cache.Items; +using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.Identity.Test.Performance.Helpers; using Microsoft.Identity.Test.Unit; @@ -60,12 +62,18 @@ public class AcquireTokenForOboCacheTests [GlobalSetup] public async Task GlobalSetupAsync() { - _cca = ConfidentialClientApplicationBuilder + var builder = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithRedirectUri(TestConstants.RedirectUri) .WithClientSecret(TestConstants.ClientSecret) - .WithLegacyCacheCompatibility(false) - .BuildConcrete(); + .WithLegacyCacheCompatibility(false); + + if (!EnableCacheSerialization) + { + builder.WithCacheOptions(new CacheOptions(0, CacheSize.TotalUsers)); + } + + _cca = builder.BuildConcrete(); if (EnableCacheSerialization) { @@ -105,6 +113,16 @@ private async Task PopulateUserCacheAsync(int totalUsers, int tokensPerUser, boo string userAssertionHash = new UserAssertion($"{TestConstants.DefaultAccessToken}{user}").AssertionHash; string homeAccountId = $"{user}.{_tenantPrefix}"; + ITokenCacheAccessor accessor; + if (enableCacheSerialization) + { + accessor = _cca.UserTokenCacheInternal.Accessor; + } + else + { + accessor = await (_cca.UserTokenCache as TokenCache).GetOrCreateAccessorAsync(userAssertionHash).ConfigureAwait(false); + } + for (int token = 0; token < tokensPerUser; token++) { string tenant = IsMultiTenant ? $"{_tenantPrefix}{token}" : _tenantPrefix; @@ -116,22 +134,22 @@ private async Task PopulateUserCacheAsync(int totalUsers, int tokensPerUser, boo homeAccountId, oboCacheKey: userAssertionHash, accessToken: TestConstants.UserAccessToken); - _cca.UserTokenCacheInternal.Accessor.SaveAccessToken(atItem); + accessor.SaveAccessToken(atItem); MsalRefreshTokenCacheItem rtItem = TokenCacheHelper.CreateRefreshTokenItem( userAssertionHash, homeAccountId, refreshToken: TestConstants.RefreshToken); - _cca.UserTokenCacheInternal.Accessor.SaveRefreshToken(rtItem); + accessor.SaveRefreshToken(rtItem); MsalIdTokenCacheItem idtItem = TokenCacheHelper.CreateIdTokenCacheItem( tenant, homeAccountId, uid: user.ToString()); - _cca.UserTokenCacheInternal.Accessor.SaveIdToken(idtItem); + accessor.SaveIdToken(idtItem); MsalAccountCacheItem accItem = TokenCacheHelper.CreateAccountItem(tenant, homeAccountId); - _cca.UserTokenCacheInternal.Accessor.SaveAccount(accItem); + accessor.SaveAccount(accItem); } if (enableCacheSerialization) @@ -149,6 +167,10 @@ private async Task PopulateUserCacheAsync(int totalUsers, int tokensPerUser, boo await _cca.UserTokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); _cca.UserTokenCacheInternal.Accessor.Clear(); } + else + { + await (_cca.UserTokenCache as TokenCache).IdentityCacheWrapper.SetUserCacheAsync(userAssertionHash, (InMemoryPartitionedUserTokenCacheAccessor)accessor, null).ConfigureAwait(false); + } } } } diff --git a/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs b/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs index c9f1f33a41..a86d743641 100644 --- a/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CacheTests/UnifiedCacheTests.cs @@ -75,7 +75,7 @@ public void UnifiedCache_MsalStoresToAndReadRtFromAdalCache() var accounts = app.UserTokenCacheInternal.GetAccountsAsync(reqParams).Result; foreach (IAccount account in accounts) { - (app.UserTokenCacheInternal as TokenCache).RemoveAccountInternal(account, requestContext); + (app.UserTokenCacheInternal as TokenCache).RemoveAccountInternalAsync(account, requestContext); } Assert.AreEqual(0, httpManager.QueueSize); diff --git a/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs new file mode 100644 index 0000000000..948f8f6e79 --- /dev/null +++ b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.ServiceEssentials; + +namespace Net5TestApp +{ + public class CompositeCacheAdapter : IIdentityCache + { + private readonly MemCacheProvider _cache = new(); + + public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) where T : ICacheObject + { + var compositeCacheEntry = await _cache.GetAsync(key).ConfigureAwait(false); + return compositeCacheEntry != null ? + new Microsoft.Identity.ServiceEssentials.CacheEntry( + (T)compositeCacheEntry.Value, + compositeCacheEntry.Expiration, + compositeCacheEntry.Refresh) : + null; + } + + public async Task> SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject + { + var cacheEntry = new CompositeCache.CacheEntry( + key, + value, + DateTimeOffset.UtcNow.Add(cacheEntryOptions.ExpirationTimeRelativeToNow), + DateTimeOffset.UtcNow.Add(cacheEntryOptions.ExpirationTimeRelativeToNow), + false); + await _cache.SetAsync(cacheEntry).ConfigureAwait(false); + + return new CacheEntry(value, DateTimeOffset.UtcNow.Add(cacheEntryOptions.ExpirationTimeRelativeToNow), DateTimeOffset.UtcNow.Add(cacheEntryOptions.RefreshTimeRelativeToNow)); + } + + #region Not Implemented + public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + #endregion + } +} diff --git a/tests/devapps/Net5TestApp/Net5TestApp.csproj b/tests/devapps/Net5TestApp/Net5TestApp.csproj index 65cad20025..e89533dce5 100644 --- a/tests/devapps/Net5TestApp/Net5TestApp.csproj +++ b/tests/devapps/Net5TestApp/Net5TestApp.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,8 +6,21 @@ Debug;Release;Debug + MobileApps + + + + + + + + + + + + Dependencies\CompositeCache.dll + diff --git a/tests/devapps/Net5TestApp/Program.cs b/tests/devapps/Net5TestApp/Program.cs index 02761c5f33..ef94efad23 100644 --- a/tests/devapps/Net5TestApp/Program.cs +++ b/tests/devapps/Net5TestApp/Program.cs @@ -1,18 +1,47 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Test.LabInfrastructure; namespace Net5TestApp { class Program { + private const string clientIdCCA = ""; + private const string thumbprint = ""; + private static readonly string authorityA = $"https://login.microsoftonline.com/organizations"; + private const string scopeGraphDefault = "https://graph.microsoft.com//.default"; + static async Task Main(string[] args) { try { - var result = await TryAuthAsync().ConfigureAwait(false); - Console.BackgroundColor = ConsoleColor.DarkGreen; - Console.WriteLine("Access Token = " + result?.AccessToken); + + var cache = new CompositeCacheAdapter(); + + var cca = ConfidentialClientApplicationBuilder + .Create(clientIdCCA) + .WithAuthority(authorityA) + .WithCertificate(CertificateHelper.FindCertificateByThumbprint(thumbprint)) + .WithCacheOptions(new CacheOptions(identityCache: cache)) + .WithLogging(MyLoggingMethod, logLevel: LogLevel.Verbose, enablePiiLogging: false) + .Build(); + + var result1 = await cca + .AcquireTokenForClient(new string[] { scopeGraphDefault }) + .ExecuteAsync() + .ConfigureAwait(true); + Console.WriteLine(result1.AuthenticationResultMetadata.TokenSource + " " + result1.AccessToken.Substring(result1.AccessToken.Length - 11, 10) + Environment.NewLine); + + var result2 = await cca + .AcquireTokenForClient(new string[] { scopeGraphDefault }) + .ExecuteAsync() + .ConfigureAwait(true); + Console.WriteLine(result2.AuthenticationResultMetadata.TokenSource + " " + result2.AccessToken.Substring(result2.AccessToken.Length - 11, 10) + Environment.NewLine); + Console.ResetColor(); } catch (MsalException e) @@ -25,21 +54,6 @@ static async Task Main(string[] args) Console.Read(); } - private static async Task TryAuthAsync() - { - var pca = PublicClientApplicationBuilder.Create("04b07795-8ddb-461a-bbee-02f9e1bf7b46") - .WithTenantId("72f988bf-86f1-41af-91ab-2d7cd011db47") - .WithDefaultRedirectUri() - .WithLogging(MyLoggingMethod, LogLevel.Info, true, false) - .Build(); - - var result = await pca.AcquireTokenInteractive(new[] { "https://storage.azure.com/.default" }) - .WithUseEmbeddedWebView(true) - .ExecuteAsync().ConfigureAwait(false); - - return result; - } - static void MyLoggingMethod(LogLevel level, string message, bool containsPii) { Console.WriteLine($"MSALTest {level} {containsPii} {message}");