From ba035689ae34b04f3eaa0d0cde1843fba53867ef Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Fri, 1 Jul 2022 01:19:18 -0700 Subject: [PATCH 1/8] Add IIdentityCache as default cache. Add user-provided IIdentityCache option. Add CompositeCache as an implementation. --- LibsAndSamples.sln | 33 +++++ src/client/Abstractions/CacheEntryOptions.cs | 131 ++++++++++++++++ .../CacheEntryOptionsExtensions.cs | 120 +++++++++++++++ .../Abstractions/DateTimeOffsetExtensions.cs | 36 +++++ src/client/Abstractions/ICacheEntry.cs | 36 +++++ src/client/Abstractions/ICacheObject.cs | 25 ++++ src/client/Abstractions/IIdentityCache.cs | 140 ++++++++++++++++++ .../Abstractions/Implementation/CacheEntry.cs | 111 ++++++++++++++ src/client/Abstractions/InternalsVisibleTo.cs | 6 + ...tity.ServiceEssentials.Abstractions.csproj | 15 ++ .../AppConfig/CacheOptions.cs | 29 ++++ .../Cache/CacheSessionManager.cs | 8 +- .../Cache/Prototype/DefaultInMemoryCache.cs | 66 +++++++++ .../Cache/Prototype/IdentityCacheWrapper.cs | 34 +++++ .../ClientApplicationBase.cs | 15 +- .../ConfidentialClientApplication.cs | 4 +- .../ITokenCacheInternal.cs | 2 +- .../Microsoft.Identity.Client.csproj | 9 +- .../TokenCache.ITokenCacheInternal.cs | 95 ++++++++---- .../Microsoft.Identity.Client/TokenCache.cs | 26 +++- .../CacheTests/UnifiedCacheTests.cs | 2 +- .../Net5TestApp/CompositeCacheAdapter.cs | 76 ++++++++++ tests/devapps/Net5TestApp/Net5TestApp.csproj | 15 +- tests/devapps/Net5TestApp/Program.cs | 47 +++--- 24 files changed, 1012 insertions(+), 69 deletions(-) create mode 100644 src/client/Abstractions/CacheEntryOptions.cs create mode 100644 src/client/Abstractions/CacheEntryOptionsExtensions.cs create mode 100644 src/client/Abstractions/DateTimeOffsetExtensions.cs create mode 100644 src/client/Abstractions/ICacheEntry.cs create mode 100644 src/client/Abstractions/ICacheObject.cs create mode 100644 src/client/Abstractions/IIdentityCache.cs create mode 100644 src/client/Abstractions/Implementation/CacheEntry.cs create mode 100644 src/client/Abstractions/InternalsVisibleTo.cs create mode 100644 src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj create mode 100644 src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs create mode 100644 src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs create mode 100644 tests/devapps/Net5TestApp/CompositeCacheAdapter.cs diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln index 27f1ced788..d77b384f91 100644 --- a/LibsAndSamples.sln +++ b/LibsAndSamples.sln @@ -5,6 +5,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B0B5396-4D95-4C15-82ED-DC22B5A3123F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client", "src\client\Microsoft.Identity.Client\Microsoft.Identity.Client.csproj", "{60117A9B-4BB8-472E-BFFF-52CBF67CA95A}" + ProjectSection(ProjectDependencies) = postProject + {57AB6744-A9D0-47BD-AEDF-D1B17639996D} = {57AB6744-A9D0-47BD-AEDF-D1B17639996D} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "devapps", "devapps", "{34BE693E-3496-45A4-B1D2-D3A0E068EEDB}" ProjectSection(SolutionItems) = preProject @@ -185,6 +188,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Intune-xamarin-Android", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client.Broker", "src\client\Microsoft.Identity.Client.Broker\Microsoft.Identity.Client.Broker.csproj", "{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.ServiceEssentials.Abstractions", "src\client\Abstractions\Microsoft.Identity.ServiceEssentials.Abstractions.csproj", "{57AB6744-A9D0-47BD-AEDF-D1B17639996D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1584,6 +1589,34 @@ Global {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x64.Build.0 = Release|Any CPU {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.ActiveCfg = Release|Any CPU {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.ActiveCfg = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.Build.0 = Debug|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.Build.0 = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.ActiveCfg = Release|Any CPU + {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/client/Abstractions/CacheEntryOptions.cs b/src/client/Abstractions/CacheEntryOptions.cs new file mode 100644 index 0000000000..26594e7bd9 --- /dev/null +++ b/src/client/Abstractions/CacheEntryOptions.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// Represents the cache options applied to an . + /// + public class CacheEntryOptions + { + private TimeSpan _timeToExpire = TimeSpan.FromHours(1); + private TimeSpan _timeToRefresh = TimeSpan.MaxValue; + private TimeSpan _timeToRemove = TimeSpan.FromHours(1); + + /// + /// Logical time-to-live for the , relative to time now. + /// + public TimeSpan TimeToExpire + { + get => _timeToExpire; + set + { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(value)); + _timeToExpire = value; + } + } + + /// + /// Emergency time-to-live for the cache item, relative to time now. + /// Represents the actual expiration time for the in a cache storage and can be used as a 'last-known-good' + /// value in scenarios when primary data source is not available. + /// + /// + /// Defaults to . + /// + public TimeSpan TimeToRemove + { + get => _timeToRemove; + set + { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(value)); + _timeToRemove = value; + } + } + + /// + /// Refresh time-to-live for the , relative to time now. + /// This value can be used in proactive refresh scenarios. + /// + /// + /// for no refresh (default). + /// + public TimeSpan TimeToRefresh + { + get => _timeToRefresh; + set + { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(value)); + _timeToRefresh = value; + } + } + + /// + /// Flag that indicates whether the should be stored only in a local cache. + /// + public bool StoreToLocalCacheOnly { get; set; } + + /// + /// Value that can be used to randomize spans in . + /// + /// + /// Negative value will be used to randomize spans to a maximum of -, + /// while positive value will be used to randomize spans to a maximum of +-. + /// + public int JitterInSeconds { get; set; } + + /// + /// Should be used for deserialization only. + /// + /// + /// will be thrown if System.Text.Json is used to + /// serialize this class on targets below .NET Core 3.0, if public parameterless constructor is not defined. + /// https://docs.microsoft.com/en-us/dotnet/core/compatibility/serialization/5.0/non-public-parameterless-constructors-not-used-for-deserialization + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This constructor is for serialization")] + public CacheEntryOptions() + { + } + + /// + /// Creates a new instance of the . + /// + /// Logical time-to-live of the cache item, relative to now. + /// + /// is set to by default. + /// is set to by default. + /// is set to 0 by default. + /// + public CacheEntryOptions(TimeSpan timeToExpire) + : this (timeToExpire, 0) + { } + + /// + /// Creates a new instance of the . + /// + /// Logical time-to-live of the cache item, relative to now. + /// + /// Negative value will be used to randomize spans to a maximum of -, + /// while positive value will be used to randomize spans to a maximum of +-. + /// + /// + /// is set to by default. + /// is set to by default. + /// + public CacheEntryOptions(TimeSpan timeToExpire, int jitterInSeconds) + { + TimeToExpire = timeToExpire; + TimeToRemove = timeToExpire; + TimeToRefresh = TimeSpan.MaxValue; + StoreToLocalCacheOnly = false; + JitterInSeconds = jitterInSeconds; + } + } +} diff --git a/src/client/Abstractions/CacheEntryOptionsExtensions.cs b/src/client/Abstractions/CacheEntryOptionsExtensions.cs new file mode 100644 index 0000000000..25cfbbaa17 --- /dev/null +++ b/src/client/Abstractions/CacheEntryOptionsExtensions.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + // CAN be made internal and moved to implementation + + /// + /// extension methods. + /// + public static class CacheEntryOptionsExtensions + { + private static readonly Random _random = new Random(); + + /// + /// Returns of + /// randomized by + /// + /// + /// instance that contains + /// and . + /// + /// + /// of + /// randomized by . + /// + /// + /// Jitter should be applied where possible, to avoid the thundering herd problem. + /// + public static TimeSpan GetTimeToRefreshWithJitter(this CacheEntryOptions cacheEntryOptions) + { + _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); + + if (cacheEntryOptions.TimeToRefresh == TimeSpan.MaxValue) + return cacheEntryOptions.TimeToRefresh; + + var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); + return cacheEntryOptions.TimeToRefresh.AddOrCap(randomSpan); + } + + /// + /// Returns of + /// randomized by + /// + /// + /// instance that contains + /// and . + /// + /// + /// of + /// randomized by . + /// + /// + /// Jitter should be applied where possible, to avoid the thundering herd problem. + /// + public static TimeSpan GetTimeToRemoveWithJitter(this CacheEntryOptions cacheEntryOptions) + { + _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); + + if (cacheEntryOptions.TimeToRemove == TimeSpan.MaxValue) + return cacheEntryOptions.TimeToRefresh; + + var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); + return cacheEntryOptions.TimeToRemove.AddOrCap(randomSpan); + } + + /// + /// Returns of + /// randomized by + /// + /// + /// instance that contains + /// and . + /// + /// + /// of + /// randomized by . + /// + /// + /// Jitter should be applied where possible, to avoid the thundering herd problem. + /// + public static TimeSpan GetTimeToExpireWithJitter(this CacheEntryOptions cacheEntryOptions) + { + _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); + + if (cacheEntryOptions.TimeToExpire == TimeSpan.MaxValue) + return cacheEntryOptions.TimeToExpire; + + var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); + return cacheEntryOptions.TimeToExpire.AddOrCap(randomSpan); + } + + private static TimeSpan GetRandomSpan(int jitterSpanInSeconds) + { + if (jitterSpanInSeconds == 0) + return TimeSpan.Zero; + else if (jitterSpanInSeconds < 0) + return TimeSpan.FromSeconds((long)((_random.NextDouble()) * jitterSpanInSeconds)); + else + return TimeSpan.FromSeconds((long)((_random.NextDouble() * 2.0 - 1.0) * jitterSpanInSeconds)); + } + + private static TimeSpan AddOrCap(this TimeSpan timeSpan1, TimeSpan timeSpan2) + { + if (timeSpan1 == TimeSpan.MaxValue || timeSpan2 == TimeSpan.MaxValue) + return TimeSpan.MaxValue; + + // checking only if timeSpan == TimeSpan.MaxValue is unsufficient + // as jitter might be applied to the timeSpan. + // based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92 + long result = timeSpan1.Ticks + timeSpan2.Ticks; + if ((timeSpan1.Ticks >> 63 == timeSpan2.Ticks >> 63) && (timeSpan1.Ticks >> 63 != result >> 63)) + return TimeSpan.MaxValue; + else + return timeSpan1.Add(timeSpan2); + } + } +} diff --git a/src/client/Abstractions/DateTimeOffsetExtensions.cs b/src/client/Abstractions/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000000..e7e8ac1e11 --- /dev/null +++ b/src/client/Abstractions/DateTimeOffsetExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + // CAN be made internal and moved to implementation + + /// + /// extension methods. + /// + public static class DateTimeOffsetExtensions + { + /// + /// Adds to . + /// If sum of and + /// exeeds , the resulting , + /// result will be set to to . + /// + public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, TimeSpan timeSpan) + { + if (dateTime == DateTimeOffset.MaxValue || timeSpan == TimeSpan.MaxValue) + return DateTimeOffset.MaxValue; + + // checking only if timeSpan == TimeSpan.MaxValue is unsufficient + // as jitter might be applied to the timeSpan. + // based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92 + long result = dateTime.UtcTicks + timeSpan.Ticks; + if ((dateTime.UtcTicks >> 63 == timeSpan.Ticks >> 63) && (dateTime.UtcTicks >> 63 != result >> 63)) + return DateTimeOffset.MaxValue; + else + return dateTime.Add(timeSpan); + } + } +} diff --git a/src/client/Abstractions/ICacheEntry.cs b/src/client/Abstractions/ICacheEntry.cs new file mode 100644 index 0000000000..6edc993b98 --- /dev/null +++ b/src/client/Abstractions/ICacheEntry.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// Represents a cache item. + /// + public interface ICacheEntry + { + /// + /// Gets the value in cache. + /// + public T Value { get; } + + /// + /// Checks if is found in a cache + /// and if it is still within its time-to-live. + /// + /// + /// if the is found and + /// is still within its time-to-live, otherwise. + /// + bool IsValid(); + + /// + /// Checks if is found in a cache + /// and can be used as a last-known-good value. + /// + /// + /// if the is found and + /// can be used as a last-known-good value, otherwise. + /// + bool IsValidAsLastKnownGood(); + } +} diff --git a/src/client/Abstractions/ICacheObject.cs b/src/client/Abstractions/ICacheObject.cs new file mode 100644 index 0000000000..187f33a96c --- /dev/null +++ b/src/client/Abstractions/ICacheObject.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// Represents an object that can be serialized and deserialized. + /// + public interface ICacheObject : IEquatable + { + /// + /// Serializes an object into a string. + /// + /// The serialized value. + string Serialize(); + + /// + /// Deserializes the . + /// + /// The serialized representation of the object. + void Deserialize(string serializedValue); + } +} diff --git a/src/client/Abstractions/IIdentityCache.cs b/src/client/Abstractions/IIdentityCache.cs new file mode 100644 index 0000000000..54781ee367 --- /dev/null +++ b/src/client/Abstractions/IIdentityCache.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// Represents a cache. + /// + public interface IIdentityCache + { + /// + /// Gets a with the given key. + /// + /// The category of the key. + /// The key for lookup in cache. + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// + /// Async task that returns the . + /// + /// + /// Caller should utilize to verify that + /// is found and withing its time-to-live. + /// + Task> GetAsync( + string category, string key, CancellationToken cancellationToken = default); + + /// + /// Gets a with the given key. + /// + /// The category of the key. + /// The key for lookup in cache. + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// + /// Async task that returns the . + /// + /// + /// Caller should utilize to verify that + /// is found and withing its time-to-live. + /// /// + Task> GetAsync( + string category, string key, CancellationToken cancellationToken = default); + + /// + /// Gets a with the given key. + /// If is valid, but should be refreshed, + /// will be invoked on a background thread. + /// If is not valid, will be invoked + /// and resulting will be stored to cache and returned. + /// + /// The category of the key. + /// The key for lookup in cache. + /// + /// + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// + /// Async task that returns the . + /// + /// + /// Caller should utilize to verify that + /// is found and withing its time-to-live. + /// + Task> GetWithRefreshFunctionAsync( + string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) + where T : ICacheObject, new(); + + /// + /// Gets a with the given key. + /// If is valid, but should be refreshed, + /// will be invoked on a background thread. + /// If is not valid, will be invoked + /// and resulting will be stored to cache and returned. + /// + /// The category of the key. + /// The key for lookup in cache. + /// + /// + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// + /// Async task that returns the . + /// + /// + /// Caller should utilize to verify that + /// is found and withing its time-to-live. + /// /// + Task> GetWithRefreshFunctionAsync( + string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default); + + /// + /// Sets the to the cache. + /// + /// The category of the key. + /// The key for lookup in cache. + /// The value to be cached. + /// Options applied when creating the . + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// Async. + Task SetAsync( + string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default); + + /// + /// Sets the to the cache. + /// + /// The category of the key. + /// The key for lookup in cache. + /// The value to be cached. + /// Options applied when creating the . + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// Async. + Task SetAsync( + string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default); + + /// + /// Removes an item from the cache with the given key. + /// + /// The category of the key. + /// The key for lookup in cache. + /// + /// Optional. The used to propagate notifications that the operation should be canceled. + /// + /// Async. + Task RemoveAsync( + string category, string key, CancellationToken cancellationToken = default); + } +} diff --git a/src/client/Abstractions/Implementation/CacheEntry.cs b/src/client/Abstractions/Implementation/CacheEntry.cs new file mode 100644 index 0000000000..b9fe589898 --- /dev/null +++ b/src/client/Abstractions/Implementation/CacheEntry.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + internal readonly struct CacheEntry : ICacheEntry, IEquatable> + { + /// + /// Gets the cache entry when this is non existing. + /// + public static CacheEntry NonExisting => new CacheEntry(); + + /// + public T Value { get; } + + public DateTimeOffset ExpirationTime { get; } + + public DateTimeOffset LastKnowGoodTime { get; } + + public DateTimeOffset RefreshTime { get; } + + public CacheEntry(T value, DateTimeOffset expirationTime, DateTimeOffset lastKnowGoodTime, DateTimeOffset refreshTime) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + ExpirationTime = expirationTime; + LastKnowGoodTime = lastKnowGoodTime; + RefreshTime = refreshTime; + } + + /// + public bool IsValid() + { + return DateTimeOffset.UtcNow < ExpirationTime; + } + + /// + public bool IsValidAsLastKnownGood() + { + return DateTimeOffset.UtcNow < LastKnowGoodTime; + } + + /// + public bool NeedsRefresh() + { + return DateTimeOffset.UtcNow >= RefreshTime; + } + + /// + /// Test for equality. + /// + /// Left hand side. + /// Right hand side. + /// true if the current object is equal to the other parameter; otherwise, false. + public static bool operator ==(CacheEntry lhs, CacheEntry rhs) + { + return lhs.Equals(rhs); + } + + /// + /// Test for inequality. + /// + /// Left hand side. + /// Right hand side. + /// true if the current object is not equal to the other parameter; otherwise, false. + public static bool operator !=(CacheEntry lhs, CacheEntry rhs) + { + return !lhs.Equals(rhs); + } + + /// + /// Generate hashcode. + /// + /// returns hashcode. + public override int GetHashCode() + { + return (Value?.GetHashCode() ?? 0) ^ + (ExpirationTime.GetHashCode()) ^ + (RefreshTime.GetHashCode()) ^ + (LastKnowGoodTime.GetHashCode()); + } + + /// + /// Checks equality. + /// + /// Object to compare. + /// if equal or not. + public override bool Equals(object obj) + { + if (obj is CacheEntry cacheEntry) + return Equals(cacheEntry); + + return false; + } + + /// + /// Checks equality. + /// + /// Object to compare. + /// if equal or not. + public bool Equals(CacheEntry other) + { + return Equals(Value, other.Value) && + ExpirationTime == other.ExpirationTime && + RefreshTime == other.RefreshTime && + LastKnowGoodTime == other.LastKnowGoodTime; + } + } +} diff --git a/src/client/Abstractions/InternalsVisibleTo.cs b/src/client/Abstractions/InternalsVisibleTo.cs new file mode 100644 index 0000000000..1233089c53 --- /dev/null +++ b/src/client/Abstractions/InternalsVisibleTo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Identity.Client, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] diff --git a/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj b/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj new file mode 100644 index 0000000000..1dd08abb1b --- /dev/null +++ b/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj @@ -0,0 +1,15 @@ + + + $(MiseVersion) + MISE Abstractions project that contains base components and interfaces. + MISE;Pipeline;Abstractions;Host;ServiceEssentials + Microsoft.Identity.ServiceEssentials + netstandard2.0 + 9 + + + + + + + diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs index 6b5d873327..b63011adf0 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Identity.ServiceEssentials; + namespace Microsoft.Identity.Client { /// @@ -39,6 +41,24 @@ public CacheOptions(bool useSharedCache) UseSharedCache = useSharedCache; } + /// + /// + /// + /// + public CacheOptions(IIdentityCache identityCache) + { + IdentityCache = identityCache; + } + + /// + /// + /// + /// + public CacheOptions(int sizeLimit) + { + SizeLimit = sizeLimit; + } + /// /// Share the cache between all ClientApplication objects. The cache becomes static. Defaults to false. /// @@ -50,5 +70,14 @@ public CacheOptions(bool useSharedCache) /// public bool UseSharedCache { get; set; } + /// + /// User-provided instance of IIdentityCache + /// + public IIdentityCache IdentityCache { get; } + + /// + /// Max count of items in the default in-memory cache with eviction + /// + public int SizeLimit { get; } } } diff --git a/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs b/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs index 3849d6f677..350ff15661 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,15 +118,15 @@ private async Task RefreshCacheForReadOperationsAsync() var args = new TokenCacheNotificationArgs( TokenCacheInternal, _requestParams.AppConfig.ClientId, - _requestParams.Account, + _requestParams.Account, hasStateChanged: false, isApplicationCache: TokenCacheInternal.IsApplicationCache, suggestedCacheKey: key, hasTokens: TokenCacheInternal.HasTokensNoLocks(), cancellationToken: _requestParams.RequestContext.UserCancellationToken, suggestedCacheExpiry: null, - correlationId: _requestParams.RequestContext.CorrelationId, - requestScopes: _requestParams.Scope, + correlationId: _requestParams.RequestContext.CorrelationId, + requestScopes: _requestParams.Scope, requestTenantId: _requestParams.AuthorityManager.OriginalAuthority.TenantId); stopwatch.Start(); 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..e0365783d9 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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) + { + ICacheEntry 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) + { + var cacheEntry = new CacheEntry(value, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); + var memoryCacheOptions = new MemoryCacheEntryOptions() + { + AbsoluteExpiration = DateTimeOffset.UtcNow.Add(cacheEntryOptions.TimeToExpire), + Size = 1 + }; + _memoryCache.Set(key, cacheEntry, memoryCacheOptions); + return Task.CompletedTask; + } + + #region Not Implemented + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) where T : ICacheObject, new() + { + throw new NotImplementedException(); + } + + public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + public Task RemoveAsync(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/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..92815fb377 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Identity.ServiceEssentials; + +namespace Microsoft.Identity.Client.Cache.Prototype +{ + internal class IdentityCacheWrapper + { + internal static IIdentityCache s_iIdentityCache { get; set; } + + internal IdentityCacheWrapper(CacheOptions cacheOptions) + { + // Set (or overwrite) cache to user-specified implementation, otherwise set to default implementation, if not already set. + s_iIdentityCache = cacheOptions?.IdentityCache ?? s_iIdentityCache ?? CreateDefaultCache(cacheOptions); + } + + private IIdentityCache CreateDefaultCache(CacheOptions cacheOptions) => new DefaultInMemoryCache(cacheOptions); + + internal async Task GetAsync(string key) + { + var entry = await s_iIdentityCache.GetAsync(string.Empty, key).ConfigureAwait(false); + return entry == null ? default : entry.Value; + } + + internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) + { + TimeSpan timeToExpire = cacheExpiry.HasValue ? cacheExpiry.Value - DateTimeOffset.UtcNow : TimeSpan.FromHours(1); + await s_iIdentityCache.SetAsync(string.Empty, key, value, new CacheEntryOptions(timeToExpire)).ConfigureAwait(false); + } + } +} diff --git a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs index cc41792069..44eabdf2a0 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); + 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 3bd44e711a..e9444c7e1e 100644 --- a/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs +++ b/src/client/Microsoft.Identity.Client/ConfidentialClientApplication.cs @@ -44,9 +44,9 @@ 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 259cb3d5c3..e6fe8ed00c 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -9,12 +9,12 @@ - net45 + @@ -33,7 +33,6 @@ 4.7.1 $(MsalClientSemVer) - true @@ -297,4 +296,8 @@ + + + + diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index c268373f97..580d9a1ba6 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.Tracing; using System.Globalization; using System.Linq; using System.Text; @@ -19,6 +18,7 @@ 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.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; @@ -165,7 +165,7 @@ async Task> IToke hasTokens: tokenCacheInternal.HasTokensNoLocks(), suggestedCacheExpiry: null, cancellationToken: requestParams.RequestContext.UserCancellationToken, - correlationId: requestParams.RequestContext.CorrelationId, + correlationId: requestParams.RequestContext.CorrelationId, requestScopes: requestParams.Scope, requestTenantId: requestParams.AuthorityManager.OriginalAuthority.TenantId); @@ -176,6 +176,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)) { @@ -186,9 +188,10 @@ async Task> IToke tenantId, msalAccessTokenCacheItem.ScopeSet, msalAccessTokenCacheItem.HomeAccountId, - msalAccessTokenCacheItem.TokenType); + msalAccessTokenCacheItem.TokenType, + accessor); - Accessor.SaveAccessToken(msalAccessTokenCacheItem); + accessor.SaveAccessToken(msalAccessTokenCacheItem); } if (idToken != null) @@ -196,20 +199,27 @@ async Task> IToke logger.Info("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("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); + await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, accessor, cacheExpiry).ConfigureAwait(false); + } SaveToLegacyAdalCache( requestParams, @@ -235,7 +245,7 @@ async Task> IToke hasTokens: tokenCacheInternal.HasTokensNoLocks(), suggestedCacheExpiry: cacheExpiry, cancellationToken: requestParams.RequestContext.UserCancellationToken, - correlationId: requestParams.RequestContext.CorrelationId, + correlationId: requestParams.RequestContext.CorrelationId, requestScopes: requestParams.Scope, requestTenantId: requestParams.AuthorityManager.OriginalAuthority.TenantId); @@ -265,7 +275,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(); @@ -388,9 +397,9 @@ private void MergeWamAccountIds(MsalAccountCacheItem msalAccountCacheItem) var existingWamAccountIds = existingAccount?.WamAccountIds; msalAccountCacheItem.WamAccountIds.MergeDifferentEntries(existingWamAccountIds); } -#endregion + #endregion -#region FindAccessToken + #region FindAccessToken /// /// IMPORTANT: this class is performance critical; any changes must be benchmarked using Microsoft.Identity.Test.Performance. /// More information about how to test and what data to look for is in https://aka.ms/msal-net-performance-testing. @@ -414,7 +423,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}"); @@ -693,7 +702,7 @@ private MsalAccessTokenCacheItem FilterTokensByPopKeyId(MsalAccessTokenCacheItem requestKid)); return null; } -#endregion + #endregion private void FilterTokensByClientId(List tokenCacheItems) where T : MsalCredentialCacheItemBase { @@ -742,7 +751,7 @@ async Task ITokenCacheInternal.FindRefreshTokenAsync( 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) @@ -1050,9 +1059,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; } @@ -1067,7 +1076,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) @@ -1104,7 +1113,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, @@ -1148,13 +1157,13 @@ async Task ITokenCacheInternal.RemoveAccountAsync(IAccount account, Authenticati correlationId: requestParameters.RequestContext.CorrelationId, requestScopes: requestParameters.Scope, requestTenantId: requestParameters.AuthorityManager.OriginalAuthority.TenantId); - + 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( @@ -1203,7 +1212,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) { @@ -1213,7 +1222,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 @@ -1228,12 +1239,12 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalRefreshTokenCacheItem refreshTokenCacheItem in refreshTokens) { - Accessor.DeleteRefreshToken(refreshTokenCacheItem); + accessor.DeleteRefreshToken(refreshTokenCacheItem); } requestContext.Logger.Info($"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) { @@ -1242,12 +1253,12 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalAccessTokenCacheItem accessTokenCacheItem in accessTokens) { - Accessor.DeleteAccessToken(accessTokenCacheItem); + accessor.DeleteAccessToken(accessTokenCacheItem); } requestContext.Logger.Info($"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) { @@ -1256,18 +1267,44 @@ bool ITokenCacheInternal.HasTokensNoLocks() foreach (MsalIdTokenCacheItem idTokenCacheItem in idTokens) { - Accessor.DeleteIdToken(idTokenCacheItem); + accessor.DeleteIdToken(idTokenCacheItem); } requestContext.Logger.Info($"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); + await IdentityCacheWrapper.SetAsync(partitionKey, accessor, cacheExpiry).ConfigureAwait(false); + } + } + + private 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 + { + var cachedAccessor = await IdentityCacheWrapper.GetAsync(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.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..a16187a604 --- /dev/null +++ b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using CompositeCache; +using Microsoft.Identity.ServiceEssentials; + +namespace Net5TestApp +{ + public class CompositeCacheAdapter : IIdentityCache + { + private MemCacheProvider _cache = new(); + + public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + var cachedResult = await _cache.GetAsync(key).ConfigureAwait(false); + return cachedResult != null ? new MsalCacheEntry((T)cachedResult.Value) : null; + } + + public async Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + { + var expirationDate = DateTimeOffset.UtcNow.Add(cacheEntryOptions.TimeToExpire); + var cacheEntry = new CacheEntry(key, value, expirationDate, expirationDate, false); + await _cache.SetAsync(cacheEntry).ConfigureAwait(false); + } + + #region Not Implemented + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) where T : ICacheObject, new() + { + throw new NotImplementedException(); + } + + public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task RemoveAsync(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 + } + + public class MsalCacheEntry : ICacheEntry + { + public MsalCacheEntry(T value) + { + Value = value; + } + + public T Value { get; } + + public bool IsValid() + { + throw new NotImplementedException(); + } + + public bool IsValidAsLastKnownGood() + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/devapps/Net5TestApp/Net5TestApp.csproj b/tests/devapps/Net5TestApp/Net5TestApp.csproj index 9f8dac242c..ebd6e43f28 100644 --- a/tests/devapps/Net5TestApp/Net5TestApp.csproj +++ b/tests/devapps/Net5TestApp/Net5TestApp.csproj @@ -1,12 +1,25 @@ - + Exe net5.0-windows10.0.17763.0 + + + + + + + + + + + + Dependencies\CompositeCache.dll + diff --git a/tests/devapps/Net5TestApp/Program.cs b/tests/devapps/Net5TestApp/Program.cs index 02761c5f33..8e1cee81be 100644 --- a/tests/devapps/Net5TestApp/Program.cs +++ b/tests/devapps/Net5TestApp/Program.cs @@ -1,18 +1,44 @@ using System; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Test.LabInfrastructure; namespace Net5TestApp { class Program { + private const string clientIdCCA = "16dab2ba-145d-4b1b-8569-bf4b9aed4dc8"; + private const string thumbprint = "4E87313FD450985A10BC0F14A292859F2DCD6CD3"; + 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 +51,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}"); From 9af44aec45c1d83a976a9c5875a9a29d6d504558 Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Thu, 14 Jul 2022 23:22:27 -0700 Subject: [PATCH 2/8] Use the updated IIDentityCache code. Use TestIdentityCache as default implementation. Set default static cache properly. --- src/client/Abstractions/CacheEntry.cs | 37 +++ src/client/Abstractions/CacheEntryOptions.cs | 101 +------- .../CacheEntryOptionsExtensions.cs | 120 ---------- .../Abstractions/DateTimeOffsetExtensions.cs | 23 +- .../Abstractions/DistributedCacheEntry.cs | 40 ++++ src/client/Abstractions/ICacheEntry.cs | 36 --- src/client/Abstractions/ICacheObject.cs | 2 +- src/client/Abstractions/IIdentityCache.cs | 69 +----- .../Abstractions/Implementation/CacheEntry.cs | 111 --------- .../Implementation/TestIdentityCache.cs | 221 ++++++++++++++++++ .../Abstractions/InMemoryCacheOptions.cs | 28 +++ .../Cache/ITokenCacheAccessor.cs | 3 +- .../Cache/Prototype/DefaultInMemoryCache.cs | 24 +- .../Cache/Prototype/IdentityCacheWrapper.cs | 49 +++- .../ClientApplicationBase.cs | 2 +- ...nMemoryPartitionedAppTokenCacheAccessor.cs | 10 + ...MemoryPartitionedUserTokenCacheAccessor.cs | 10 + .../TokenCache.ITokenCacheInternal.cs | 2 + .../Net5TestApp/CompositeCacheAdapter.cs | 54 ++--- 19 files changed, 451 insertions(+), 491 deletions(-) create mode 100644 src/client/Abstractions/CacheEntry.cs delete mode 100644 src/client/Abstractions/CacheEntryOptionsExtensions.cs create mode 100644 src/client/Abstractions/DistributedCacheEntry.cs delete mode 100644 src/client/Abstractions/ICacheEntry.cs delete mode 100644 src/client/Abstractions/Implementation/CacheEntry.cs create mode 100644 src/client/Abstractions/Implementation/TestIdentityCache.cs create mode 100644 src/client/Abstractions/InMemoryCacheOptions.cs diff --git a/src/client/Abstractions/CacheEntry.cs b/src/client/Abstractions/CacheEntry.cs new file mode 100644 index 0000000000..a936eb7f2f --- /dev/null +++ b/src/client/Abstractions/CacheEntry.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// Represents a cache item. Can grow if needed. + /// + public class CacheEntry + { + /// + /// Gets the value in cache. + /// + public T Value { get; } + + /// + /// TODO: backpropagate ?? max category count? internal DistributedCacheEntry to carry details (we need it, not anyone else) + /// + public DateTimeOffset ExpirationTimeUTC { get; } + + /// + /// + public DateTimeOffset RefreshTimeUTC { get; } + + /// + /// + /// + public CacheEntry(T value, DateTimeOffset expirationTimeUTC, DateTimeOffset refreshTimeUTC) + { + Value = value; + ExpirationTimeUTC = expirationTimeUTC; + RefreshTimeUTC = refreshTimeUTC; + } + } +} diff --git a/src/client/Abstractions/CacheEntryOptions.cs b/src/client/Abstractions/CacheEntryOptions.cs index 26594e7bd9..207ba39c63 100644 --- a/src/client/Abstractions/CacheEntryOptions.cs +++ b/src/client/Abstractions/CacheEntryOptions.cs @@ -2,72 +2,23 @@ // Licensed under the MIT License. using System; -using System.ComponentModel; namespace Microsoft.Identity.ServiceEssentials { /// - /// Represents the cache options applied to an . + /// . /// public class CacheEntryOptions { - private TimeSpan _timeToExpire = TimeSpan.FromHours(1); - private TimeSpan _timeToRefresh = TimeSpan.MaxValue; - private TimeSpan _timeToRemove = TimeSpan.FromHours(1); - /// - /// Logical time-to-live for the , relative to time now. /// - public TimeSpan TimeToExpire - { - get => _timeToExpire; - set - { - if (value <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(value)); - _timeToExpire = value; - } - } + public DateTimeOffset ExpirationTimeUTC { get; } /// - /// Emergency time-to-live for the cache item, relative to time now. - /// Represents the actual expiration time for the in a cache storage and can be used as a 'last-known-good' - /// value in scenarios when primary data source is not available. /// - /// - /// Defaults to . - /// - public TimeSpan TimeToRemove - { - get => _timeToRemove; - set - { - if (value <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(value)); - _timeToRemove = value; - } - } - - /// - /// Refresh time-to-live for the , relative to time now. - /// This value can be used in proactive refresh scenarios. - /// - /// - /// for no refresh (default). - /// - public TimeSpan TimeToRefresh - { - get => _timeToRefresh; - set - { - if (value <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(value)); - _timeToRefresh = value; - } - } + public DateTimeOffset RefreshTimeUTC { get; } /// - /// Flag that indicates whether the should be stored only in a local cache. /// public bool StoreToLocalCacheOnly { get; set; } @@ -81,51 +32,23 @@ public TimeSpan TimeToRefresh public int JitterInSeconds { get; set; } /// - /// Should be used for deserialization only. + /// If default was not provided for a category. /// - /// - /// will be thrown if System.Text.Json is used to - /// serialize this class on targets below .NET Core 3.0, if public parameterless constructor is not defined. - /// https://docs.microsoft.com/en-us/dotnet/core/compatibility/serialization/5.0/non-public-parameterless-constructors-not-used-for-deserialization - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This constructor is for serialization")] - public CacheEntryOptions() - { - } + public int MaxCategoryCount { get; set; } /// - /// Creates a new instance of the . /// - /// Logical time-to-live of the cache item, relative to now. - /// - /// is set to by default. - /// is set to by default. - /// is set to 0 by default. - /// - public CacheEntryOptions(TimeSpan timeToExpire) - : this (timeToExpire, 0) - { } + public CacheEntryOptions(DateTimeOffset expirationTimeUTC, int maxCategoryCount) : this(expirationTimeUTC, DateTimeOffset.MaxValue, maxCategoryCount) + { + } /// - /// Creates a new instance of the . /// - /// Logical time-to-live of the cache item, relative to now. - /// - /// Negative value will be used to randomize spans to a maximum of -, - /// while positive value will be used to randomize spans to a maximum of +-. - /// - /// - /// is set to by default. - /// is set to by default. - /// - public CacheEntryOptions(TimeSpan timeToExpire, int jitterInSeconds) + public CacheEntryOptions(DateTimeOffset expirationTimeUTC, DateTimeOffset refreshTimeUTC, int maxCategoryCount) { - TimeToExpire = timeToExpire; - TimeToRemove = timeToExpire; - TimeToRefresh = TimeSpan.MaxValue; - StoreToLocalCacheOnly = false; - JitterInSeconds = jitterInSeconds; + ExpirationTimeUTC = expirationTimeUTC; + MaxCategoryCount = maxCategoryCount; + RefreshTimeUTC = refreshTimeUTC; } } } diff --git a/src/client/Abstractions/CacheEntryOptionsExtensions.cs b/src/client/Abstractions/CacheEntryOptionsExtensions.cs deleted file mode 100644 index 25cfbbaa17..0000000000 --- a/src/client/Abstractions/CacheEntryOptionsExtensions.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - // CAN be made internal and moved to implementation - - /// - /// extension methods. - /// - public static class CacheEntryOptionsExtensions - { - private static readonly Random _random = new Random(); - - /// - /// Returns of - /// randomized by - /// - /// - /// instance that contains - /// and . - /// - /// - /// of - /// randomized by . - /// - /// - /// Jitter should be applied where possible, to avoid the thundering herd problem. - /// - public static TimeSpan GetTimeToRefreshWithJitter(this CacheEntryOptions cacheEntryOptions) - { - _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); - - if (cacheEntryOptions.TimeToRefresh == TimeSpan.MaxValue) - return cacheEntryOptions.TimeToRefresh; - - var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); - return cacheEntryOptions.TimeToRefresh.AddOrCap(randomSpan); - } - - /// - /// Returns of - /// randomized by - /// - /// - /// instance that contains - /// and . - /// - /// - /// of - /// randomized by . - /// - /// - /// Jitter should be applied where possible, to avoid the thundering herd problem. - /// - public static TimeSpan GetTimeToRemoveWithJitter(this CacheEntryOptions cacheEntryOptions) - { - _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); - - if (cacheEntryOptions.TimeToRemove == TimeSpan.MaxValue) - return cacheEntryOptions.TimeToRefresh; - - var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); - return cacheEntryOptions.TimeToRemove.AddOrCap(randomSpan); - } - - /// - /// Returns of - /// randomized by - /// - /// - /// instance that contains - /// and . - /// - /// - /// of - /// randomized by . - /// - /// - /// Jitter should be applied where possible, to avoid the thundering herd problem. - /// - public static TimeSpan GetTimeToExpireWithJitter(this CacheEntryOptions cacheEntryOptions) - { - _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); - - if (cacheEntryOptions.TimeToExpire == TimeSpan.MaxValue) - return cacheEntryOptions.TimeToExpire; - - var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds); - return cacheEntryOptions.TimeToExpire.AddOrCap(randomSpan); - } - - private static TimeSpan GetRandomSpan(int jitterSpanInSeconds) - { - if (jitterSpanInSeconds == 0) - return TimeSpan.Zero; - else if (jitterSpanInSeconds < 0) - return TimeSpan.FromSeconds((long)((_random.NextDouble()) * jitterSpanInSeconds)); - else - return TimeSpan.FromSeconds((long)((_random.NextDouble() * 2.0 - 1.0) * jitterSpanInSeconds)); - } - - private static TimeSpan AddOrCap(this TimeSpan timeSpan1, TimeSpan timeSpan2) - { - if (timeSpan1 == TimeSpan.MaxValue || timeSpan2 == TimeSpan.MaxValue) - return TimeSpan.MaxValue; - - // checking only if timeSpan == TimeSpan.MaxValue is unsufficient - // as jitter might be applied to the timeSpan. - // based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92 - long result = timeSpan1.Ticks + timeSpan2.Ticks; - if ((timeSpan1.Ticks >> 63 == timeSpan2.Ticks >> 63) && (timeSpan1.Ticks >> 63 != result >> 63)) - return TimeSpan.MaxValue; - else - return timeSpan1.Add(timeSpan2); - } - } -} diff --git a/src/client/Abstractions/DateTimeOffsetExtensions.cs b/src/client/Abstractions/DateTimeOffsetExtensions.cs index e7e8ac1e11..ee7c57aeeb 100644 --- a/src/client/Abstractions/DateTimeOffsetExtensions.cs +++ b/src/client/Abstractions/DateTimeOffsetExtensions.cs @@ -12,17 +12,18 @@ namespace Microsoft.Identity.ServiceEssentials /// public static class DateTimeOffsetExtensions { + private static readonly Random _random = new Random(); + /// - /// Adds to . - /// If sum of and - /// exeeds , the resulting , - /// result will be set to to . /// - public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, TimeSpan timeSpan) + public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, int seconds) { - if (dateTime == DateTimeOffset.MaxValue || timeSpan == TimeSpan.MaxValue) + var timeSpan = GetRandomSpan(seconds); + + if (dateTime == DateTimeOffset.MaxValue) return DateTimeOffset.MaxValue; + // safeguard // checking only if timeSpan == TimeSpan.MaxValue is unsufficient // as jitter might be applied to the timeSpan. // based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92 @@ -32,5 +33,15 @@ public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, TimeSpan tim else return dateTime.Add(timeSpan); } + + private static TimeSpan GetRandomSpan(int jitterSpanInSeconds) + { + if (jitterSpanInSeconds == 0) + return TimeSpan.Zero; + else if (jitterSpanInSeconds < 0) + return TimeSpan.FromSeconds((long)((_random.NextDouble()) * jitterSpanInSeconds)); + else + return TimeSpan.FromSeconds((long)((_random.NextDouble() * 2.0 - 1.0) * jitterSpanInSeconds)); + } } } diff --git a/src/client/Abstractions/DistributedCacheEntry.cs b/src/client/Abstractions/DistributedCacheEntry.cs new file mode 100644 index 0000000000..ee728bc458 --- /dev/null +++ b/src/client/Abstractions/DistributedCacheEntry.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.ServiceEssentials +{ + internal class DistributedCacheEntry + { + public DistributedCacheEntry() { } + + public T Value { get; set; } + + /// + /// + public DateTimeOffset ExpirationTimeUTC { get; set; } + + /// + /// + public DateTimeOffset RefreshTimeUTC { get; set; } + + public int MaxCategoryCount { get; set; } + + public int JitterInSeconds { get; set; } + + public void Deserialize(string serializedValue) + { + _ = serializedValue; + _ = MaxCategoryCount; + // todo + } + + public string Serialize() + { + _ = MaxCategoryCount; + // todo + return string.Empty; + } + } +} diff --git a/src/client/Abstractions/ICacheEntry.cs b/src/client/Abstractions/ICacheEntry.cs deleted file mode 100644 index 6edc993b98..0000000000 --- a/src/client/Abstractions/ICacheEntry.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// Represents a cache item. - /// - public interface ICacheEntry - { - /// - /// Gets the value in cache. - /// - public T Value { get; } - - /// - /// Checks if is found in a cache - /// and if it is still within its time-to-live. - /// - /// - /// if the is found and - /// is still within its time-to-live, otherwise. - /// - bool IsValid(); - - /// - /// Checks if is found in a cache - /// and can be used as a last-known-good value. - /// - /// - /// if the is found and - /// can be used as a last-known-good value, otherwise. - /// - bool IsValidAsLastKnownGood(); - } -} diff --git a/src/client/Abstractions/ICacheObject.cs b/src/client/Abstractions/ICacheObject.cs index 187f33a96c..02dc38cf55 100644 --- a/src/client/Abstractions/ICacheObject.cs +++ b/src/client/Abstractions/ICacheObject.cs @@ -8,7 +8,7 @@ namespace Microsoft.Identity.ServiceEssentials /// /// Represents an object that can be serialized and deserialized. /// - public interface ICacheObject : IEquatable + public interface ICacheObject { /// /// Serializes an object into a string. diff --git a/src/client/Abstractions/IIdentityCache.cs b/src/client/Abstractions/IIdentityCache.cs index 54781ee367..8ce323546a 100644 --- a/src/client/Abstractions/IIdentityCache.cs +++ b/src/client/Abstractions/IIdentityCache.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; using System.Threading; using System.Threading.Tasks; @@ -13,7 +12,7 @@ namespace Microsoft.Identity.ServiceEssentials public interface IIdentityCache { /// - /// Gets a with the given key. + /// Gets a with the given key. /// /// The category of the key. /// The key for lookup in cache. @@ -21,14 +20,13 @@ public interface IIdentityCache /// Optional. The used to propagate notifications that the operation should be canceled. /// /// - /// Async task that returns the . + /// Async task that returns the . /// /// - /// Caller should utilize to verify that - /// is found and withing its time-to-live. /// - Task> GetAsync( - string category, string key, CancellationToken cancellationToken = default); + Task> GetAsync( + string category, string key, CancellationToken cancellationToken = default) + where T : ICacheObject; /// /// Gets a with the given key. @@ -39,64 +37,12 @@ Task> GetAsync( /// Optional. The used to propagate notifications that the operation should be canceled. /// /// - /// Async task that returns the . /// /// - /// Caller should utilize to verify that - /// is found and withing its time-to-live. /// /// - Task> GetAsync( + Task> GetAsync( string category, string key, CancellationToken cancellationToken = default); - /// - /// Gets a with the given key. - /// If is valid, but should be refreshed, - /// will be invoked on a background thread. - /// If is not valid, will be invoked - /// and resulting will be stored to cache and returned. - /// - /// The category of the key. - /// The key for lookup in cache. - /// - /// - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// - /// Async task that returns the . - /// - /// - /// Caller should utilize to verify that - /// is found and withing its time-to-live. - /// - Task> GetWithRefreshFunctionAsync( - string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) - where T : ICacheObject, new(); - - /// - /// Gets a with the given key. - /// If is valid, but should be refreshed, - /// will be invoked on a background thread. - /// If is not valid, will be invoked - /// and resulting will be stored to cache and returned. - /// - /// The category of the key. - /// The key for lookup in cache. - /// - /// - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// - /// Async task that returns the . - /// - /// - /// Caller should utilize to verify that - /// is found and withing its time-to-live. - /// /// - Task> GetWithRefreshFunctionAsync( - string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default); - /// /// Sets the to the cache. /// @@ -109,7 +55,8 @@ Task> GetWithRefreshFunctionAsync( /// /// Async. Task SetAsync( - string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default); + string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + where T : ICacheObject; /// /// Sets the to the cache. diff --git a/src/client/Abstractions/Implementation/CacheEntry.cs b/src/client/Abstractions/Implementation/CacheEntry.cs deleted file mode 100644 index b9fe589898..0000000000 --- a/src/client/Abstractions/Implementation/CacheEntry.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - internal readonly struct CacheEntry : ICacheEntry, IEquatable> - { - /// - /// Gets the cache entry when this is non existing. - /// - public static CacheEntry NonExisting => new CacheEntry(); - - /// - public T Value { get; } - - public DateTimeOffset ExpirationTime { get; } - - public DateTimeOffset LastKnowGoodTime { get; } - - public DateTimeOffset RefreshTime { get; } - - public CacheEntry(T value, DateTimeOffset expirationTime, DateTimeOffset lastKnowGoodTime, DateTimeOffset refreshTime) - { - Value = value ?? throw new ArgumentNullException(nameof(value)); - ExpirationTime = expirationTime; - LastKnowGoodTime = lastKnowGoodTime; - RefreshTime = refreshTime; - } - - /// - public bool IsValid() - { - return DateTimeOffset.UtcNow < ExpirationTime; - } - - /// - public bool IsValidAsLastKnownGood() - { - return DateTimeOffset.UtcNow < LastKnowGoodTime; - } - - /// - public bool NeedsRefresh() - { - return DateTimeOffset.UtcNow >= RefreshTime; - } - - /// - /// Test for equality. - /// - /// Left hand side. - /// Right hand side. - /// true if the current object is equal to the other parameter; otherwise, false. - public static bool operator ==(CacheEntry lhs, CacheEntry rhs) - { - return lhs.Equals(rhs); - } - - /// - /// Test for inequality. - /// - /// Left hand side. - /// Right hand side. - /// true if the current object is not equal to the other parameter; otherwise, false. - public static bool operator !=(CacheEntry lhs, CacheEntry rhs) - { - return !lhs.Equals(rhs); - } - - /// - /// Generate hashcode. - /// - /// returns hashcode. - public override int GetHashCode() - { - return (Value?.GetHashCode() ?? 0) ^ - (ExpirationTime.GetHashCode()) ^ - (RefreshTime.GetHashCode()) ^ - (LastKnowGoodTime.GetHashCode()); - } - - /// - /// Checks equality. - /// - /// Object to compare. - /// if equal or not. - public override bool Equals(object obj) - { - if (obj is CacheEntry cacheEntry) - return Equals(cacheEntry); - - return false; - } - - /// - /// Checks equality. - /// - /// Object to compare. - /// if equal or not. - public bool Equals(CacheEntry other) - { - return Equals(Value, other.Value) && - ExpirationTime == other.ExpirationTime && - RefreshTime == other.RefreshTime && - LastKnowGoodTime == other.LastKnowGoodTime; - } - } -} diff --git a/src/client/Abstractions/Implementation/TestIdentityCache.cs b/src/client/Abstractions/Implementation/TestIdentityCache.cs new file mode 100644 index 0000000000..c42cdb919c --- /dev/null +++ b/src/client/Abstractions/Implementation/TestIdentityCache.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Microsoft.Identity.ServiceEssentials.Implementation +{ + /// + /// Starting a sample... + /// + public class TestIdentityCache : IIdentityCache, IDisposable + { + private bool disposed; + + private readonly Dictionary memoryCaches + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly IDistributedCache _distributedCache; + + // also takes IIdentityLogger, ITelemetryClient + // when IDistributedCache is present, takes also IEncryptionProvider + + /// + /// + /// + /// + public TestIdentityCache(InMemoryCacheOptions inMemoryCacheOptions) + { + _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); + + foreach (var option in inMemoryCacheOptions.CategoryOptions) + { + memoryCaches[option.Key] = new MemoryCache(option.Value); + } + } + + /// + /// + /// + public TestIdentityCache(IOptions inMemoryCacheOptions) + { + _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); + + foreach (var option in inMemoryCacheOptions.Value.CategoryOptions) + { + memoryCaches[option.Key] = new MemoryCache(option.Value); + } + } + + /// + /// + /// + /// + public TestIdentityCache(IOptions inMemoryCacheOptions, IDistributedCache distributedCache) + { + _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); + _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); + + foreach (var option in inMemoryCacheOptions.Value.CategoryOptions) + { + memoryCaches[option.Key] = new MemoryCache(option.Value); + } + } + + /// + public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) where T : ICacheObject + { + return await GetAsyncInternalAsync(category, key, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + { + return await GetAsyncInternalAsync(category, key, cancellationToken).ConfigureAwait(false); + } + + private async Task> GetAsyncInternalAsync(string category, string key, CancellationToken cancellationToken) + { + var cache = GetMemoryCache(category); + + CacheEntry result = null; + if (cache?.TryGetValue(key, out result) == true) + return result; + else if (_distributedCache != null) + { + var l2CacheValue = await _distributedCache.GetStringAsync(key, cancellationToken).ConfigureAwait(false); + if (l2CacheValue == null) + return null; + + // todo: decrypt + + DistributedCacheEntry entry = new DistributedCacheEntry(); + entry.Deserialize(l2CacheValue); + + // propagate to L1 + SetToMemoryCacheInternal(category, key, entry.Value, new CacheEntryOptions(entry.ExpirationTimeUTC, entry.RefreshTimeUTC, entry.MaxCategoryCount) { JitterInSeconds = entry.JitterInSeconds }); + + if (cache?.TryGetValue(key, out result) == true) + return result; + else + return null; + } + + return null; + } + + /// + public async Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) + { + var cache = GetMemoryCache(category); + cache?.Remove(key); + if (_distributedCache != null) + await _distributedCache.RemoveAsync(key, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject + { + await SetAsyncInternalAsync(category, key, value, cacheEntryOptions, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + { + await SetAsyncInternalAsync(category, key, value, cacheEntryOptions, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task SetAsyncInternalAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken) + { + _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); + SetToMemoryCacheInternal(category, key, value, cacheEntryOptions); + + if (_distributedCache != null) + { + // set to L2 too + var distributedCacheEntry = new DistributedCacheEntry() + { + Value = value, + ExpirationTimeUTC = cacheEntryOptions.ExpirationTimeUTC, + RefreshTimeUTC = cacheEntryOptions.RefreshTimeUTC, + MaxCategoryCount = cacheEntryOptions.MaxCategoryCount, + JitterInSeconds = cacheEntryOptions.JitterInSeconds + }; + string serializedCacheEntry = distributedCacheEntry.Serialize(); + // todo: encrypt + await _distributedCache.SetStringAsync(key, serializedCacheEntry, cancellationToken).ConfigureAwait(false); + } + } + + internal void SetToMemoryCacheInternal(string category, string key, T value, CacheEntryOptions cacheEntryOptions) + { + var cache = GetOrCreateMemoryCache(category, cacheEntryOptions); + + // apply jitter + var expirationTime = cacheEntryOptions.ExpirationTimeUTC.AddOrCap(cacheEntryOptions.JitterInSeconds); + var refreshTime = cacheEntryOptions.RefreshTimeUTC.AddOrCap(cacheEntryOptions.JitterInSeconds); + + var cacheEntry = new CacheEntry(value, expirationTime, refreshTime); + var memoryCacheOptions = new MemoryCacheEntryOptions() + { + AbsoluteExpiration = expirationTime, + Size = 1 + }; + cache?.Set(key, cacheEntry, memoryCacheOptions); + } + + private MemoryCache GetMemoryCache(string category) + { + if (memoryCaches.TryGetValue(category, out var cache)) + return cache; + + + return null; + } + + private MemoryCache GetOrCreateMemoryCache(string category, CacheEntryOptions cacheEntryOptions) + { + if (memoryCaches.TryGetValue(category, out var cache)) + return cache; + + memoryCaches[category] = new MemoryCache(new MemoryCacheOptions() { SizeLimit = cacheEntryOptions.MaxCategoryCount }); + + return memoryCaches[category]; + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) below. + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose pattern. + /// + /// Whether this is called by user code. + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + foreach (var memoryCache in memoryCaches) + { + memoryCache.Value.Dispose(); + } + } + + disposed = true; + } + } + } +} diff --git a/src/client/Abstractions/InMemoryCacheOptions.cs b/src/client/Abstractions/InMemoryCacheOptions.cs new file mode 100644 index 0000000000..54ba87d463 --- /dev/null +++ b/src/client/Abstractions/InMemoryCacheOptions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Microsoft.Identity.ServiceEssentials +{ + /// + /// + public class InMemoryCacheOptions : IOptions + { + /// + /// category settings + /// +#pragma warning disable CA2227 // Collection properties should be read only + public IDictionary CategoryOptions { get; set; } = new Dictionary(); +#pragma warning restore CA2227 // Collection properties should be read only + + /// + /// + /// + InMemoryCacheOptions IOptions.Value => this; + } +} 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 index e0365783d9..de78e930b5 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs @@ -18,19 +18,19 @@ public DefaultInMemoryCache(CacheOptions cacheOptions) _memoryCache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = cacheOptions?.SizeLimit ?? 1000 }); } - public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) where T : ICacheObject { - ICacheEntry result = null; + 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) + public Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject { - var cacheEntry = new CacheEntry(value, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); + var cacheEntry = new CacheEntry(value, cacheEntryOptions.ExpirationTimeUTC, cacheEntryOptions.RefreshTimeUTC); var memoryCacheOptions = new MemoryCacheEntryOptions() { - AbsoluteExpiration = DateTimeOffset.UtcNow.Add(cacheEntryOptions.TimeToExpire), + AbsoluteExpiration = cacheEntryOptions.ExpirationTimeUTC, Size = 1 }; _memoryCache.Set(key, cacheEntry, memoryCacheOptions); @@ -38,21 +38,11 @@ public Task SetAsync(string category, string key, T value, CacheEntryOptions } #region Not Implemented - public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) where T : ICacheObject, new() - { - throw new NotImplementedException(); - } - - public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) + public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs index 92815fb377..ad98a28981 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -2,33 +2,66 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Identity.ServiceEssentials; +using Microsoft.Identity.ServiceEssentials.Implementation; namespace Microsoft.Identity.Client.Cache.Prototype { internal class IdentityCacheWrapper { - internal static IIdentityCache s_iIdentityCache { get; set; } + private static CacheOptions s_cacheOptions; + private readonly IIdentityCache _identityCache; + private static readonly Lazy s_defaultIIdentityCache = new Lazy( + () => CreateDefaultCache()); + // 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) { + s_cacheOptions = cacheOptions; // Set (or overwrite) cache to user-specified implementation, otherwise set to default implementation, if not already set. - s_iIdentityCache = cacheOptions?.IdentityCache ?? s_iIdentityCache ?? CreateDefaultCache(cacheOptions); + + if (cacheOptions.IdentityCache != null) + { + _identityCache = cacheOptions?.IdentityCache; + } + else if (cacheOptions.UseSharedCache) + { + _identityCache = s_defaultIIdentityCache.Value; + } + else + { + _identityCache = CreateDefaultCache(); + } } - private IIdentityCache CreateDefaultCache(CacheOptions cacheOptions) => new DefaultInMemoryCache(cacheOptions); + private static IIdentityCache CreateDefaultCache() + { + var memoryCachesOptions = new InMemoryCacheOptions() + { + CategoryOptions = new Dictionary() + { + { "tokens", new MemoryCacheOptions() {SizeLimit = s_cacheOptions.SizeLimit} } + } + }; + + return new TestIdentityCache(memoryCachesOptions); + } - internal async Task GetAsync(string key) + internal async Task GetAsync(string key) where T : ICacheObject { - var entry = await s_iIdentityCache.GetAsync(string.Empty, key).ConfigureAwait(false); + var entry = await _identityCache.GetAsync(string.Empty, key).ConfigureAwait(false); return entry == null ? default : entry.Value; } - internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) + internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject { - TimeSpan timeToExpire = cacheExpiry.HasValue ? cacheExpiry.Value - DateTimeOffset.UtcNow : TimeSpan.FromHours(1); - await s_iIdentityCache.SetAsync(string.Empty, key, value, new CacheEntryOptions(timeToExpire)).ConfigureAwait(false); + await _identityCache.SetAsync(string.Empty, key, value, new CacheEntryOptions(cacheExpiry ?? DateTimeOffset.UtcNow, 1)).ConfigureAwait(false); } } } diff --git a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs index 44eabdf2a0..d312a53279 100644 --- a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs +++ b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs @@ -74,7 +74,7 @@ internal ClientApplicationBase(ApplicationConfiguration 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); + IdentityCacheWrapper = new IdentityCacheWrapper(config.AccessorOptions ?? new CacheOptions()); if (config.UserTokenLegacyCachePersistenceForTest != null) { diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs index 56e9337a0c..d98f666a7a 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs @@ -228,5 +228,15 @@ public virtual bool HasAccessOrRefreshTokens() { return AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer())); } + + public string Serialize() + { + throw new NotImplementedException(); + } + + public void Deserialize(string 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 bb72ebd4d4..e412577f4f 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs @@ -295,5 +295,15 @@ public virtual bool HasAccessOrRefreshTokens() return RefreshTokenCacheDictionary.Any(partition => partition.Value.Count > 0) || AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer())); } + + public string Serialize() + { + throw new NotImplementedException(); + } + + public void Deserialize(string 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 580d9a1ba6..48a91210c9 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -1288,6 +1288,8 @@ bool ITokenCacheInternal.HasTokensNoLocks() } } + // Cache setup is validated to be mutually exclusive - + // Token cache serialization is allowed only when WithCacheOptions is not used. private async Task GetOrCreateAccessorAsync(string partitionKey) { // If user set up legacy cache serialization, then use old accessor instance (it would have been populated with tokens) diff --git a/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs index a16187a604..e6c6529cdb 100644 --- a/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs +++ b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs @@ -11,38 +11,32 @@ namespace Net5TestApp { public class CompositeCacheAdapter : IIdentityCache { - private MemCacheProvider _cache = new(); + private readonly MemCacheProvider _cache = new(); - public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) + public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) where T : ICacheObject { - var cachedResult = await _cache.GetAsync(key).ConfigureAwait(false); - return cachedResult != null ? new MsalCacheEntry((T)cachedResult.Value) : null; + 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) + public async Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject { - var expirationDate = DateTimeOffset.UtcNow.Add(cacheEntryOptions.TimeToExpire); - var cacheEntry = new CacheEntry(key, value, expirationDate, expirationDate, false); + var cacheEntry = new CompositeCache.CacheEntry(key, value, cacheEntryOptions.ExpirationTimeUTC, cacheEntryOptions.ExpirationTimeUTC, false); await _cache.SetAsync(cacheEntry).ConfigureAwait(false); - } - #region Not Implemented - public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); } - public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) where T : ICacheObject, new() - { - throw new NotImplementedException(); - } - - public Task> GetWithRefreshFunctionAsync(string category, string key, CacheEntryOptions cacheEntryOptions, Func> refreshFunction, CancellationToken cancellationToken = default) + #region Not Implemented + public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - - public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) + public Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } @@ -53,24 +47,4 @@ public Task SetAsync(string category, string key, string value, CacheEntryOption } #endregion } - - public class MsalCacheEntry : ICacheEntry - { - public MsalCacheEntry(T value) - { - Value = value; - } - - public T Value { get; } - - public bool IsValid() - { - throw new NotImplementedException(); - } - - public bool IsValidAsLastKnownGood() - { - throw new NotImplementedException(); - } - } } From 59d77da9649e99e3f98016e03d720ec3cea77adb Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Fri, 29 Jul 2022 12:28:13 -0700 Subject: [PATCH 3/8] Implement new IdentityCache changes and import cache projects. --- LibsAndSamples.sln | 91 +++++--- src/client/Abstractions/CacheEntry.cs | 37 --- src/client/Abstractions/CacheEntryOptions.cs | 54 ----- .../Abstractions/DateTimeOffsetExtensions.cs | 47 ---- .../Abstractions/DistributedCacheEntry.cs | 40 ---- src/client/Abstractions/ICacheObject.cs | 25 -- src/client/Abstractions/IIdentityCache.cs | 87 ------- .../Implementation/TestIdentityCache.cs | 221 ------------------ .../Abstractions/InMemoryCacheOptions.cs | 28 --- src/client/Abstractions/InternalsVisibleTo.cs | 6 - ...tity.ServiceEssentials.Abstractions.csproj | 15 -- .../Cache/Prototype/DefaultInMemoryCache.cs | 15 +- .../Cache/Prototype/IdentityCacheWrapper.cs | 22 +- .../ClientApplicationBase.cs | 2 +- .../Microsoft.Identity.Client.csproj | 5 +- ...nMemoryPartitionedAppTokenCacheAccessor.cs | 4 +- ...MemoryPartitionedUserTokenCacheAccessor.cs | 4 +- .../Net5TestApp/CompositeCacheAdapter.cs | 11 +- 18 files changed, 98 insertions(+), 616 deletions(-) delete mode 100644 src/client/Abstractions/CacheEntry.cs delete mode 100644 src/client/Abstractions/CacheEntryOptions.cs delete mode 100644 src/client/Abstractions/DateTimeOffsetExtensions.cs delete mode 100644 src/client/Abstractions/DistributedCacheEntry.cs delete mode 100644 src/client/Abstractions/ICacheObject.cs delete mode 100644 src/client/Abstractions/IIdentityCache.cs delete mode 100644 src/client/Abstractions/Implementation/TestIdentityCache.cs delete mode 100644 src/client/Abstractions/InMemoryCacheOptions.cs delete mode 100644 src/client/Abstractions/InternalsVisibleTo.cs delete mode 100644 src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln index d77b384f91..6c5d38907f 100644 --- a/LibsAndSamples.sln +++ b/LibsAndSamples.sln @@ -6,7 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B0B5396 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client", "src\client\Microsoft.Identity.Client\Microsoft.Identity.Client.csproj", "{60117A9B-4BB8-472E-BFFF-52CBF67CA95A}" ProjectSection(ProjectDependencies) = postProject - {57AB6744-A9D0-47BD-AEDF-D1B17639996D} = {57AB6744-A9D0-47BD-AEDF-D1B17639996D} + {C300F77C-C8B6-4A10-BBF8-29C96497FB51} = {C300F77C-C8B6-4A10-BBF8-29C96497FB51} + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9} = {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9} EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "devapps", "devapps", "{34BE693E-3496-45A4-B1D2-D3A0E068EEDB}" @@ -188,7 +189,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Intune-xamarin-Android", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client.Broker", "src\client\Microsoft.Identity.Client.Broker\Microsoft.Identity.Client.Broker.csproj", "{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.ServiceEssentials.Abstractions", "src\client\Abstractions\Microsoft.Identity.ServiceEssentials.Abstractions.csproj", "{57AB6744-A9D0-47BD-AEDF-D1B17639996D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.ServiceEssentials.Abstractions", "..\MISE\src\Abstractions\Microsoft.Identity.ServiceEssentials.Abstractions.csproj", "{C300F77C-C8B6-4A10-BBF8-29C96497FB51}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.ServiceEssentials.IdentityCache", "..\MISE\src\IdentityCache\Microsoft.Identity.ServiceEssentials.IdentityCache.csproj", "{EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1589,34 +1592,62 @@ Global {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x64.Build.0 = Release|Any CPU {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.ActiveCfg = Release|Any CPU {6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.ActiveCfg = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.Build.0 = Debug|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.Build.0 = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.ActiveCfg = Release|Any CPU - {57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|ARM.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|ARM64.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|iPhone.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|x64.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|x64.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|x86.ActiveCfg = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Debug|x86.Build.0 = Debug|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|Any CPU.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|ARM.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|ARM.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|ARM64.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|ARM64.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|iPhone.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|iPhone.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|x64.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|x64.Build.0 = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|x86.ActiveCfg = Release|Any CPU + {C300F77C-C8B6-4A10-BBF8-29C96497FB51}.Release|x86.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|ARM.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|ARM.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|ARM64.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|iPhone.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|x64.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Debug|x86.Build.0 = Debug|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|Any CPU.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|ARM.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|ARM.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|ARM64.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|ARM64.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|iPhone.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|iPhone.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|x64.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|x64.Build.0 = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|x86.ActiveCfg = Release|Any CPU + {EFEF5FA8-0FDA-4B4D-BF57-C65BBC54DAC9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/client/Abstractions/CacheEntry.cs b/src/client/Abstractions/CacheEntry.cs deleted file mode 100644 index a936eb7f2f..0000000000 --- a/src/client/Abstractions/CacheEntry.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// Represents a cache item. Can grow if needed. - /// - public class CacheEntry - { - /// - /// Gets the value in cache. - /// - public T Value { get; } - - /// - /// TODO: backpropagate ?? max category count? internal DistributedCacheEntry to carry details (we need it, not anyone else) - /// - public DateTimeOffset ExpirationTimeUTC { get; } - - /// - /// - public DateTimeOffset RefreshTimeUTC { get; } - - /// - /// - /// - public CacheEntry(T value, DateTimeOffset expirationTimeUTC, DateTimeOffset refreshTimeUTC) - { - Value = value; - ExpirationTimeUTC = expirationTimeUTC; - RefreshTimeUTC = refreshTimeUTC; - } - } -} diff --git a/src/client/Abstractions/CacheEntryOptions.cs b/src/client/Abstractions/CacheEntryOptions.cs deleted file mode 100644 index 207ba39c63..0000000000 --- a/src/client/Abstractions/CacheEntryOptions.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// . - /// - public class CacheEntryOptions - { - /// - /// - public DateTimeOffset ExpirationTimeUTC { get; } - - /// - /// - public DateTimeOffset RefreshTimeUTC { get; } - - /// - /// - public bool StoreToLocalCacheOnly { get; set; } - - /// - /// Value that can be used to randomize spans in . - /// - /// - /// Negative value will be used to randomize spans to a maximum of -, - /// while positive value will be used to randomize spans to a maximum of +-. - /// - public int JitterInSeconds { get; set; } - - /// - /// If default was not provided for a category. - /// - public int MaxCategoryCount { get; set; } - - /// - /// - public CacheEntryOptions(DateTimeOffset expirationTimeUTC, int maxCategoryCount) : this(expirationTimeUTC, DateTimeOffset.MaxValue, maxCategoryCount) - { - } - - /// - /// - public CacheEntryOptions(DateTimeOffset expirationTimeUTC, DateTimeOffset refreshTimeUTC, int maxCategoryCount) - { - ExpirationTimeUTC = expirationTimeUTC; - MaxCategoryCount = maxCategoryCount; - RefreshTimeUTC = refreshTimeUTC; - } - } -} diff --git a/src/client/Abstractions/DateTimeOffsetExtensions.cs b/src/client/Abstractions/DateTimeOffsetExtensions.cs deleted file mode 100644 index ee7c57aeeb..0000000000 --- a/src/client/Abstractions/DateTimeOffsetExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - // CAN be made internal and moved to implementation - - /// - /// extension methods. - /// - public static class DateTimeOffsetExtensions - { - private static readonly Random _random = new Random(); - - /// - /// - public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, int seconds) - { - var timeSpan = GetRandomSpan(seconds); - - if (dateTime == DateTimeOffset.MaxValue) - return DateTimeOffset.MaxValue; - - // safeguard - // checking only if timeSpan == TimeSpan.MaxValue is unsufficient - // as jitter might be applied to the timeSpan. - // based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92 - long result = dateTime.UtcTicks + timeSpan.Ticks; - if ((dateTime.UtcTicks >> 63 == timeSpan.Ticks >> 63) && (dateTime.UtcTicks >> 63 != result >> 63)) - return DateTimeOffset.MaxValue; - else - return dateTime.Add(timeSpan); - } - - private static TimeSpan GetRandomSpan(int jitterSpanInSeconds) - { - if (jitterSpanInSeconds == 0) - return TimeSpan.Zero; - else if (jitterSpanInSeconds < 0) - return TimeSpan.FromSeconds((long)((_random.NextDouble()) * jitterSpanInSeconds)); - else - return TimeSpan.FromSeconds((long)((_random.NextDouble() * 2.0 - 1.0) * jitterSpanInSeconds)); - } - } -} diff --git a/src/client/Abstractions/DistributedCacheEntry.cs b/src/client/Abstractions/DistributedCacheEntry.cs deleted file mode 100644 index ee728bc458..0000000000 --- a/src/client/Abstractions/DistributedCacheEntry.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - internal class DistributedCacheEntry - { - public DistributedCacheEntry() { } - - public T Value { get; set; } - - /// - /// - public DateTimeOffset ExpirationTimeUTC { get; set; } - - /// - /// - public DateTimeOffset RefreshTimeUTC { get; set; } - - public int MaxCategoryCount { get; set; } - - public int JitterInSeconds { get; set; } - - public void Deserialize(string serializedValue) - { - _ = serializedValue; - _ = MaxCategoryCount; - // todo - } - - public string Serialize() - { - _ = MaxCategoryCount; - // todo - return string.Empty; - } - } -} diff --git a/src/client/Abstractions/ICacheObject.cs b/src/client/Abstractions/ICacheObject.cs deleted file mode 100644 index 02dc38cf55..0000000000 --- a/src/client/Abstractions/ICacheObject.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// Represents an object that can be serialized and deserialized. - /// - public interface ICacheObject - { - /// - /// Serializes an object into a string. - /// - /// The serialized value. - string Serialize(); - - /// - /// Deserializes the . - /// - /// The serialized representation of the object. - void Deserialize(string serializedValue); - } -} diff --git a/src/client/Abstractions/IIdentityCache.cs b/src/client/Abstractions/IIdentityCache.cs deleted file mode 100644 index 8ce323546a..0000000000 --- a/src/client/Abstractions/IIdentityCache.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// Represents a cache. - /// - public interface IIdentityCache - { - /// - /// Gets a with the given key. - /// - /// The category of the key. - /// The key for lookup in cache. - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// - /// Async task that returns the . - /// - /// - /// - Task> GetAsync( - string category, string key, CancellationToken cancellationToken = default) - where T : ICacheObject; - - /// - /// Gets a with the given key. - /// - /// The category of the key. - /// The key for lookup in cache. - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// - /// - /// - /// /// - Task> GetAsync( - string category, string key, CancellationToken cancellationToken = default); - - /// - /// Sets the to the cache. - /// - /// The category of the key. - /// The key for lookup in cache. - /// The value to be cached. - /// Options applied when creating the . - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// Async. - Task SetAsync( - string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) - where T : ICacheObject; - - /// - /// Sets the to the cache. - /// - /// The category of the key. - /// The key for lookup in cache. - /// The value to be cached. - /// Options applied when creating the . - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// Async. - Task SetAsync( - string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default); - - /// - /// Removes an item from the cache with the given key. - /// - /// The category of the key. - /// The key for lookup in cache. - /// - /// Optional. The used to propagate notifications that the operation should be canceled. - /// - /// Async. - Task RemoveAsync( - string category, string key, CancellationToken cancellationToken = default); - } -} diff --git a/src/client/Abstractions/Implementation/TestIdentityCache.cs b/src/client/Abstractions/Implementation/TestIdentityCache.cs deleted file mode 100644 index c42cdb919c..0000000000 --- a/src/client/Abstractions/Implementation/TestIdentityCache.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace Microsoft.Identity.ServiceEssentials.Implementation -{ - /// - /// Starting a sample... - /// - public class TestIdentityCache : IIdentityCache, IDisposable - { - private bool disposed; - - private readonly Dictionary memoryCaches - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - private readonly IDistributedCache _distributedCache; - - // also takes IIdentityLogger, ITelemetryClient - // when IDistributedCache is present, takes also IEncryptionProvider - - /// - /// - /// - /// - public TestIdentityCache(InMemoryCacheOptions inMemoryCacheOptions) - { - _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); - - foreach (var option in inMemoryCacheOptions.CategoryOptions) - { - memoryCaches[option.Key] = new MemoryCache(option.Value); - } - } - - /// - /// - /// - public TestIdentityCache(IOptions inMemoryCacheOptions) - { - _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); - - foreach (var option in inMemoryCacheOptions.Value.CategoryOptions) - { - memoryCaches[option.Key] = new MemoryCache(option.Value); - } - } - - /// - /// - /// - /// - public TestIdentityCache(IOptions inMemoryCacheOptions, IDistributedCache distributedCache) - { - _ = inMemoryCacheOptions ?? throw new ArgumentNullException(nameof(inMemoryCacheOptions)); - _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); - - foreach (var option in inMemoryCacheOptions.Value.CategoryOptions) - { - memoryCaches[option.Key] = new MemoryCache(option.Value); - } - } - - /// - public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) where T : ICacheObject - { - return await GetAsyncInternalAsync(category, key, cancellationToken).ConfigureAwait(false); - } - - /// - public async Task> GetAsync(string category, string key, CancellationToken cancellationToken = default) - { - return await GetAsyncInternalAsync(category, key, cancellationToken).ConfigureAwait(false); - } - - private async Task> GetAsyncInternalAsync(string category, string key, CancellationToken cancellationToken) - { - var cache = GetMemoryCache(category); - - CacheEntry result = null; - if (cache?.TryGetValue(key, out result) == true) - return result; - else if (_distributedCache != null) - { - var l2CacheValue = await _distributedCache.GetStringAsync(key, cancellationToken).ConfigureAwait(false); - if (l2CacheValue == null) - return null; - - // todo: decrypt - - DistributedCacheEntry entry = new DistributedCacheEntry(); - entry.Deserialize(l2CacheValue); - - // propagate to L1 - SetToMemoryCacheInternal(category, key, entry.Value, new CacheEntryOptions(entry.ExpirationTimeUTC, entry.RefreshTimeUTC, entry.MaxCategoryCount) { JitterInSeconds = entry.JitterInSeconds }); - - if (cache?.TryGetValue(key, out result) == true) - return result; - else - return null; - } - - return null; - } - - /// - public async Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) - { - var cache = GetMemoryCache(category); - cache?.Remove(key); - if (_distributedCache != null) - await _distributedCache.RemoveAsync(key, cancellationToken).ConfigureAwait(false); - } - - /// - public async Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject - { - await SetAsyncInternalAsync(category, key, value, cacheEntryOptions, cancellationToken).ConfigureAwait(false); - } - - /// - public async Task SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) - { - await SetAsyncInternalAsync(category, key, value, cacheEntryOptions, cancellationToken).ConfigureAwait(false); - } - - /// - public async Task SetAsyncInternalAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken) - { - _ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions)); - SetToMemoryCacheInternal(category, key, value, cacheEntryOptions); - - if (_distributedCache != null) - { - // set to L2 too - var distributedCacheEntry = new DistributedCacheEntry() - { - Value = value, - ExpirationTimeUTC = cacheEntryOptions.ExpirationTimeUTC, - RefreshTimeUTC = cacheEntryOptions.RefreshTimeUTC, - MaxCategoryCount = cacheEntryOptions.MaxCategoryCount, - JitterInSeconds = cacheEntryOptions.JitterInSeconds - }; - string serializedCacheEntry = distributedCacheEntry.Serialize(); - // todo: encrypt - await _distributedCache.SetStringAsync(key, serializedCacheEntry, cancellationToken).ConfigureAwait(false); - } - } - - internal void SetToMemoryCacheInternal(string category, string key, T value, CacheEntryOptions cacheEntryOptions) - { - var cache = GetOrCreateMemoryCache(category, cacheEntryOptions); - - // apply jitter - var expirationTime = cacheEntryOptions.ExpirationTimeUTC.AddOrCap(cacheEntryOptions.JitterInSeconds); - var refreshTime = cacheEntryOptions.RefreshTimeUTC.AddOrCap(cacheEntryOptions.JitterInSeconds); - - var cacheEntry = new CacheEntry(value, expirationTime, refreshTime); - var memoryCacheOptions = new MemoryCacheEntryOptions() - { - AbsoluteExpiration = expirationTime, - Size = 1 - }; - cache?.Set(key, cacheEntry, memoryCacheOptions); - } - - private MemoryCache GetMemoryCache(string category) - { - if (memoryCaches.TryGetValue(category, out var cache)) - return cache; - - - return null; - } - - private MemoryCache GetOrCreateMemoryCache(string category, CacheEntryOptions cacheEntryOptions) - { - if (memoryCaches.TryGetValue(category, out var cache)) - return cache; - - memoryCaches[category] = new MemoryCache(new MemoryCacheOptions() { SizeLimit = cacheEntryOptions.MaxCategoryCount }); - - return memoryCaches[category]; - } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) below. - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose pattern. - /// - /// Whether this is called by user code. - protected virtual void Dispose(bool disposing) - { - if (!disposed) - { - if (disposing) - { - foreach (var memoryCache in memoryCaches) - { - memoryCache.Value.Dispose(); - } - } - - disposed = true; - } - } - } -} diff --git a/src/client/Abstractions/InMemoryCacheOptions.cs b/src/client/Abstractions/InMemoryCacheOptions.cs deleted file mode 100644 index 54ba87d463..0000000000 --- a/src/client/Abstractions/InMemoryCacheOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace Microsoft.Identity.ServiceEssentials -{ - /// - /// - public class InMemoryCacheOptions : IOptions - { - /// - /// category settings - /// -#pragma warning disable CA2227 // Collection properties should be read only - public IDictionary CategoryOptions { get; set; } = new Dictionary(); -#pragma warning restore CA2227 // Collection properties should be read only - - /// - /// - /// - InMemoryCacheOptions IOptions.Value => this; - } -} diff --git a/src/client/Abstractions/InternalsVisibleTo.cs b/src/client/Abstractions/InternalsVisibleTo.cs deleted file mode 100644 index 1233089c53..0000000000 --- a/src/client/Abstractions/InternalsVisibleTo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.Identity.Client, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")] diff --git a/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj b/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj deleted file mode 100644 index 1dd08abb1b..0000000000 --- a/src/client/Abstractions/Microsoft.Identity.ServiceEssentials.Abstractions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - $(MiseVersion) - MISE Abstractions project that contains base components and interfaces. - MISE;Pipeline;Abstractions;Host;ServiceEssentials - Microsoft.Identity.ServiceEssentials - netstandard2.0 - 9 - - - - - - - diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs index de78e930b5..64bcd96fa6 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs @@ -18,23 +18,24 @@ 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 + 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 + public Task> SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + where T : ICacheObject { - var cacheEntry = new CacheEntry(value, cacheEntryOptions.ExpirationTimeUTC, cacheEntryOptions.RefreshTimeUTC); + var cacheEntry = new CacheEntry(value, DateTimeOffset.UtcNow.Add(cacheEntryOptions.ExpirationTimeRelativeToNow), DateTimeOffset.UtcNow.Add(cacheEntryOptions.RefreshTimeRelativeToNow)); var memoryCacheOptions = new MemoryCacheEntryOptions() { - AbsoluteExpiration = cacheEntryOptions.ExpirationTimeUTC, + AbsoluteExpiration = cacheEntry.ExpirationTimeUTC, Size = 1 }; - _memoryCache.Set(key, cacheEntry, memoryCacheOptions); - return Task.CompletedTask; + return Task.FromResult(_memoryCache.Set(key, cacheEntry, memoryCacheOptions)); } #region Not Implemented @@ -47,7 +48,7 @@ public Task> GetAsync(string category, string key, Cancellati throw new NotImplementedException(); } - public Task SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) + public Task> SetAsync(string category, string key, string value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs index ad98a28981..6b8bc78ef7 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Identity.ServiceEssentials; -using Microsoft.Identity.ServiceEssentials.Implementation; +using Microsoft.Identity.ServiceEssentials.IdentityCache; +using Microsoft.IdentityModel.Abstractions; namespace Microsoft.Identity.Client.Cache.Prototype { @@ -14,16 +14,19 @@ 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 CategoryName = "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) + 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) @@ -42,26 +45,27 @@ internal IdentityCacheWrapper(CacheOptions cacheOptions) private static IIdentityCache CreateDefaultCache() { - var memoryCachesOptions = new InMemoryCacheOptions() + var memoryCacheOptions = new InMemoryCacheOptions() { - CategoryOptions = new Dictionary() + MaxNumberOfItemsForCategory = new Dictionary() { - { "tokens", new MemoryCacheOptions() {SizeLimit = s_cacheOptions.SizeLimit} } + { CategoryName, s_cacheOptions.SizeLimit }, } }; - return new TestIdentityCache(memoryCachesOptions); + return new IdentityCachePrototype(memoryCacheOptions, _identityLogger, null); } internal async Task GetAsync(string key) where T : ICacheObject { - var entry = await _identityCache.GetAsync(string.Empty, key).ConfigureAwait(false); + var entry = await _identityCache.GetAsync(CategoryName, key).ConfigureAwait(false); return entry == null ? default : entry.Value; } internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject { - await _identityCache.SetAsync(string.Empty, key, value, new CacheEntryOptions(cacheExpiry ?? DateTimeOffset.UtcNow, 1)).ConfigureAwait(false); + TimeSpan expirationTimeRelativeToNow = cacheExpiry.HasValue ? cacheExpiry.Value - DateTimeOffset.UtcNow : TimeSpan.FromHours(1); + await _identityCache.SetAsync(CategoryName, 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 d312a53279..eb9692f828 100644 --- a/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs +++ b/src/client/Microsoft.Identity.Client/ClientApplicationBase.cs @@ -74,7 +74,7 @@ internal ClientApplicationBase(ApplicationConfiguration 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()); + IdentityCacheWrapper = new IdentityCacheWrapper(config.AccessorOptions ?? new CacheOptions(), ServiceBundle.Config.IdentityLogger); if (config.UserTokenLegacyCachePersistenceForTest != null) { diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index e6fe8ed00c..ea3eee01f4 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -294,10 +294,11 @@ - + - + + diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs index d98f666a7a..e4b24c714e 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs @@ -229,12 +229,12 @@ public virtual bool HasAccessOrRefreshTokens() return AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer())); } - public string Serialize() + public byte[] Serialize() { throw new NotImplementedException(); } - public void Deserialize(string serializedValue) + 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 e412577f4f..43d21ec023 100644 --- a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs +++ b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs @@ -296,12 +296,12 @@ public virtual bool HasAccessOrRefreshTokens() AccessTokenCacheDictionary.Any(partition => partition.Value.Any(token => !token.Value.IsExpiredWithBuffer())); } - public string Serialize() + public byte[] Serialize() { throw new NotImplementedException(); } - public void Deserialize(string serializedValue) + public void Deserialize(byte[] serializedValue) { throw new NotImplementedException(); } diff --git a/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs index e6c6529cdb..948f8f6e79 100644 --- a/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs +++ b/tests/devapps/Net5TestApp/CompositeCacheAdapter.cs @@ -4,7 +4,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using CompositeCache; using Microsoft.Identity.ServiceEssentials; namespace Net5TestApp @@ -24,11 +23,17 @@ public class CompositeCacheAdapter : IIdentityCache null; } - public async Task SetAsync(string category, string key, T value, CacheEntryOptions cacheEntryOptions, CancellationToken cancellationToken = default) where T : ICacheObject + 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, cacheEntryOptions.ExpirationTimeUTC, cacheEntryOptions.ExpirationTimeUTC, false); + 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 From f1a226a5b4c1b266eef21cdd9b89e5a64815aae9 Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Mon, 3 Oct 2022 23:47:20 -0700 Subject: [PATCH 4/8] Typo. --- tests/devapps/Net5TestApp/Program.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/devapps/Net5TestApp/Program.cs b/tests/devapps/Net5TestApp/Program.cs index 8e1cee81be..ef94efad23 100644 --- a/tests/devapps/Net5TestApp/Program.cs +++ b/tests/devapps/Net5TestApp/Program.cs @@ -1,4 +1,7 @@ -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; @@ -7,8 +10,8 @@ namespace Net5TestApp { class Program { - private const string clientIdCCA = "16dab2ba-145d-4b1b-8569-bf4b9aed4dc8"; - private const string thumbprint = "4E87313FD450985A10BC0F14A292859F2DCD6CD3"; + 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"; From 8a3fd9061367979f2dbb4d117c804cdcf869ef0e Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Thu, 6 Oct 2022 23:13:09 -0700 Subject: [PATCH 5/8] Update the integration with common cache to store concrete ITokenCacheAccessor types. --- .../Cache/Prototype/DefaultInMemoryCache.cs | 6 +- .../Cache/Prototype/IdentityCacheWrapper.cs | 4 +- ...nMemoryPartitionedAppTokenCacheAccessor.cs | 8 ++- ...MemoryPartitionedUserTokenCacheAccessor.cs | 8 ++- .../TokenCache.ITokenCacheInternal.cs | 62 ++++++++++++++----- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs index 64bcd96fa6..f68e7999b9 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/DefaultInMemoryCache.cs @@ -1,6 +1,7 @@ // 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; @@ -38,7 +39,7 @@ public Task> SetAsync(string category, string key, T value, Cac return Task.FromResult(_memoryCache.Set(key, cacheEntry, memoryCacheOptions)); } - #region Not Implemented +#region Not Implemented public Task RemoveAsync(string category, string key, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -52,6 +53,7 @@ public Task> SetAsync(string category, string key, string val { throw new NotImplementedException(); } - #endregion +#endregion } } +#endif diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs index 6b8bc78ef7..0372c57723 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -56,13 +56,13 @@ private static IIdentityCache CreateDefaultCache() return new IdentityCachePrototype(memoryCacheOptions, _identityLogger, null); } - internal async Task GetAsync(string key) where T : ICacheObject + internal async Task GetAsync(string key) where T : ICacheObject, new() { var entry = await _identityCache.GetAsync(CategoryName, key).ConfigureAwait(false); return entry == null ? default : entry.Value; } - internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject + internal async Task SetAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new() { TimeSpan expirationTimeRelativeToNow = cacheExpiry.HasValue ? cacheExpiry.Value - DateTimeOffset.UtcNow : TimeSpan.FromHours(1); await _identityCache.SetAsync(CategoryName, key, value, new CacheEntryOptions(expirationTimeRelativeToNow, 1)).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedAppTokenCacheAccessor.cs index edfb195eef..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) diff --git a/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs b/src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/InMemoryPartitionedUserTokenCacheAccessor.cs index 76f5bfb4a7..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)); diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index 1ef53364ac..061f2ed1b6 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -19,6 +19,7 @@ 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; @@ -187,7 +188,7 @@ async Task> IToke requestParams.RequestContext.ApiEvent.DurationInCacheInMs += sw.ElapsedMilliseconds; } - var accessor = await GetOrCreateAccessorAsync(suggestedWebCacheKey).ConfigureAwait(false); + var accessor = await GetOrCreateAccessorAsync(suggestedWebCacheKey, requestParams).ConfigureAwait(false); // Don't cache PoP access tokens from broker if (msalAccessTokenCacheItem != null && !(response.TokenSource == TokenSource.Broker && response.TokenType == Constants.PoPAuthHeaderPrefix)) @@ -229,7 +230,14 @@ async Task> IToke if (!((ITokenCacheInternal)this).IsAppSubscribedToSerializationEvents()) { DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, logger); - await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, accessor, cacheExpiry).ConfigureAwait(false); + if (accessor is InMemoryPartitionedAppTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } } SaveToLegacyAdalCache( @@ -395,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; } @@ -435,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 = (await GetOrCreateAccessorAsync(partitionKey).ConfigureAwait(false)).GetAllAccessTokens(partitionKey, logger); + var accessTokens = (await GetOrCreateAccessorAsync(partitionKey, requestParams).ConfigureAwait(false)).GetAllAccessTokens(partitionKey, logger); requestParams.RequestContext.Logger.Always($"[FindAccessTokenAsync] Discovered {accessTokens.Count} access tokens in cache using partition key: {partitionKey}"); @@ -492,7 +502,8 @@ private static void FilterTokensByScopes( !OAuth2Value.ReservedScopes.Contains(s)); tokenCacheItems.FilterWithLogging( - item => { + item => + { bool accepted = ScopeHelper.ScopeContains(item.ScopeSet, requestScopes); if (logger.IsLoggingEnabled(LogLevel.Verbose)) @@ -761,10 +772,12 @@ async Task ITokenCacheInternal.FindRefreshTokenAsync( string familyId) { if (requestParams.Authority == null) + { return null; + } var requestKey = CacheKeyFactory.GetKeyFromRequest(requestParams); - var refreshTokens = (await GetOrCreateAccessorAsync(requestKey).ConfigureAwait(false)).GetAllRefreshTokens(requestKey); + var refreshTokens = (await GetOrCreateAccessorAsync(requestKey, requestParams).ConfigureAwait(false)).GetAllRefreshTokens(requestKey); requestParams.RequestContext.Logger.Always($"[FindRefreshTokenAsync] Discovered {refreshTokens.Count} refresh tokens in cache using key: {requestKey}"); if (refreshTokens.Count != 0) @@ -922,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( @@ -955,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) @@ -1029,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; @@ -1074,7 +1093,7 @@ private void UpdateWithAdalAccountsWithoutClientInfo( async Task ITokenCacheInternal.GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem msalAccessTokenCacheItem) { - var idToken = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetIdToken(msalAccessTokenCacheItem); + var idToken = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem), null).ConfigureAwait(false)).GetIdToken(msalAccessTokenCacheItem); return idToken; } @@ -1089,7 +1108,7 @@ private async Task> GetTenantProfilesAsync( Debug.Assert(homeAccountId != null); - var idTokenCacheItems = (await GetOrCreateAccessorAsync(homeAccountId).ConfigureAwait(false)).GetAllIdTokens(homeAccountId); + var idTokenCacheItems = (await GetOrCreateAccessorAsync(homeAccountId, requestParameters).ConfigureAwait(false)).GetAllIdTokens(homeAccountId); FilterTokensByClientId(idTokenCacheItems); if (!requestParameters.AppConfig.MultiCloudSupportEnabled) @@ -1126,7 +1145,7 @@ async Task ITokenCacheInternal.GetAccountAssociatedWithAccessTokenAsync var tenantProfiles = await GetTenantProfilesAsync(requestParameters, msalAccessTokenCacheItem.HomeAccountId).ConfigureAwait(false); - var accountCacheItem = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetAccount( + var accountCacheItem = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem), requestParameters).ConfigureAwait(false)).GetAccount( new MsalAccountCacheKey( msalAccessTokenCacheItem.Environment, msalAccessTokenCacheItem.TenantId, @@ -1173,7 +1192,6 @@ 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); } @@ -1209,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); } } @@ -1239,7 +1256,7 @@ bool ITokenCacheInternal.HasTokensNoLocks() string partitionKey = account.HomeAccountId.Identifier; - var accessor = await GetOrCreateAccessorAsync(partitionKey).ConfigureAwait(false); + var accessor = await GetOrCreateAccessorAsync(partitionKey, null).ConfigureAwait(false); var refreshTokens = accessor.GetAllRefreshTokens(partitionKey); refreshTokens.RemoveAll(item => !item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase)); @@ -1301,13 +1318,20 @@ bool ITokenCacheInternal.HasTokensNoLocks() if (!((ITokenCacheInternal)this).IsAppSubscribedToSerializationEvents()) { DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, requestContext.Logger); - await IdentityCacheWrapper.SetAsync(partitionKey, accessor, cacheExpiry).ConfigureAwait(false); + if (accessor is InMemoryPartitionedAppTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAsync(partitionKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + } + else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) + { + await IdentityCacheWrapper.SetAsync(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. - private async Task GetOrCreateAccessorAsync(string partitionKey) + private async Task GetOrCreateAccessorAsync(string partitionKey, AuthenticationRequestParameters requestParams) { // 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. @@ -1317,7 +1341,17 @@ private async Task GetOrCreateAccessorAsync(string partitio } else { - var cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); + ITokenCacheAccessor cachedAccessor = null; + + if (requestParams != null && requestParams.IsClientCredentialRequest) + { + cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); + } + else + { + cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); + } + if (cachedAccessor == null) { var proxy = ServiceBundle?.PlatformProxy ?? PlatformProxyFactory.CreatePlatformProxy(null); From 1357510322456237015bcfa91841156f063c49bd Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Fri, 7 Oct 2022 00:04:27 -0700 Subject: [PATCH 6/8] Add reference to common cache packages. --- .../Microsoft.Identity.Client.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 8428bb012c..95773acde4 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -324,6 +324,8 @@ - + + + From 81e5aaee80aaca3bf29a0bc930352136014758c1 Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Mon, 10 Oct 2022 00:25:14 -0700 Subject: [PATCH 7/8] Update OBO and client perf tests. --- .../TokenCache.ITokenCacheInternal.cs | 21 ++++++----- .../AcquireTokenForClientCacheTests.cs | 29 ++++++++++++--- .../AcquireTokenForOboCacheTests.cs | 36 +++++++++++++++---- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index 061f2ed1b6..79f8da9437 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -188,7 +188,7 @@ async Task> IToke requestParams.RequestContext.ApiEvent.DurationInCacheInMs += sw.ElapsedMilliseconds; } - var accessor = await GetOrCreateAccessorAsync(suggestedWebCacheKey, requestParams).ConfigureAwait(false); + 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)) @@ -445,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 = (await GetOrCreateAccessorAsync(partitionKey, requestParams).ConfigureAwait(false)).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}"); @@ -777,7 +777,7 @@ async Task ITokenCacheInternal.FindRefreshTokenAsync( } var requestKey = CacheKeyFactory.GetKeyFromRequest(requestParams); - var refreshTokens = (await GetOrCreateAccessorAsync(requestKey, requestParams).ConfigureAwait(false)).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) @@ -1093,7 +1093,7 @@ private void UpdateWithAdalAccountsWithoutClientInfo( async Task ITokenCacheInternal.GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem msalAccessTokenCacheItem) { - var idToken = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem), null).ConfigureAwait(false)).GetIdToken(msalAccessTokenCacheItem); + var idToken = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetIdToken(msalAccessTokenCacheItem); return idToken; } @@ -1108,7 +1108,7 @@ private async Task> GetTenantProfilesAsync( Debug.Assert(homeAccountId != null); - var idTokenCacheItems = (await GetOrCreateAccessorAsync(homeAccountId, requestParameters).ConfigureAwait(false)).GetAllIdTokens(homeAccountId); + var idTokenCacheItems = (await GetOrCreateAccessorAsync(homeAccountId).ConfigureAwait(false)).GetAllIdTokens(homeAccountId); FilterTokensByClientId(idTokenCacheItems); if (!requestParameters.AppConfig.MultiCloudSupportEnabled) @@ -1145,7 +1145,7 @@ async Task ITokenCacheInternal.GetAccountAssociatedWithAccessTokenAsync var tenantProfiles = await GetTenantProfilesAsync(requestParameters, msalAccessTokenCacheItem.HomeAccountId).ConfigureAwait(false); - var accountCacheItem = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem), requestParameters).ConfigureAwait(false)).GetAccount( + var accountCacheItem = (await GetOrCreateAccessorAsync(CacheKeyFactory.GetIdTokenKeyFromCachedItem(msalAccessTokenCacheItem)).ConfigureAwait(false)).GetAccount( new MsalAccountCacheKey( msalAccessTokenCacheItem.Environment, msalAccessTokenCacheItem.TenantId, @@ -1256,7 +1256,7 @@ bool ITokenCacheInternal.HasTokensNoLocks() string partitionKey = account.HomeAccountId.Identifier; - var accessor = await GetOrCreateAccessorAsync(partitionKey, null).ConfigureAwait(false); + var accessor = await GetOrCreateAccessorAsync(partitionKey).ConfigureAwait(false); var refreshTokens = accessor.GetAllRefreshTokens(partitionKey); refreshTokens.RemoveAll(item => !item.HomeAccountId.Equals(account.HomeAccountId.Identifier, StringComparison.OrdinalIgnoreCase)); @@ -1331,7 +1331,7 @@ bool ITokenCacheInternal.HasTokensNoLocks() // Cache setup is validated to be mutually exclusive - // Token cache serialization is allowed only when WithCacheOptions is not used. - private async Task GetOrCreateAccessorAsync(string partitionKey, AuthenticationRequestParameters requestParams) + 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. @@ -1341,9 +1341,8 @@ private async Task GetOrCreateAccessorAsync(string partitio } else { - ITokenCacheAccessor cachedAccessor = null; - - if (requestParams != null && requestParams.IsClientCredentialRequest) + ITokenCacheAccessor cachedAccessor; + if (IsAppTokenCache) { cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); } diff --git a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs index 03a0b4afee..d150d8ed1e 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)); + } + + _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.SetAsync(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..d8be1ae28e 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(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.SetAsync(userAssertionHash, (InMemoryPartitionedUserTokenCacheAccessor)accessor, null).ConfigureAwait(false); + } } } } From 939ca1f9f7bb03d5baccf55b486939b43738acae Mon Sep 17 00:00:00 2001 From: pmaytak <34331512+pmaytak@users.noreply.github.com> Date: Mon, 10 Oct 2022 14:47:37 -0700 Subject: [PATCH 8/8] Add two size limits, app and user tokens, for two categories in IIdentityCache. --- .../AppConfig/CacheOptions.cs | 20 +++++++---- .../Cache/Prototype/IdentityCacheWrapper.cs | 34 +++++++++++++++---- .../TokenCache.ITokenCacheInternal.cs | 12 +++---- .../AcquireTokenForClientCacheTests.cs | 4 +-- .../AcquireTokenForOboCacheTests.cs | 4 +-- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs index b63011adf0..2fcbc6780d 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/CacheOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using Microsoft.Identity.ServiceEssentials; namespace Microsoft.Identity.Client @@ -47,16 +48,18 @@ public CacheOptions(bool useSharedCache) /// public CacheOptions(IIdentityCache identityCache) { - IdentityCache = identityCache; + IdentityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); } /// /// /// - /// - public CacheOptions(int sizeLimit) + /// + /// + public CacheOptions(int appTokenCacheSizeLimit, int userTokenCacheSizeLimit) { - SizeLimit = sizeLimit; + AppTokenCacheSizeLimit = appTokenCacheSizeLimit > 0 ? appTokenCacheSizeLimit : 0; + UserTokenCacheSizeLimit = userTokenCacheSizeLimit > 0 ? userTokenCacheSizeLimit : 0; } /// @@ -76,8 +79,13 @@ public CacheOptions(int sizeLimit) public IIdentityCache IdentityCache { get; } /// - /// Max count of items in the default in-memory cache with eviction + /// 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 SizeLimit { get; } + public int UserTokenCacheSizeLimit { get; } } } diff --git a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs index 0372c57723..42b46dc4b3 100644 --- a/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs +++ b/src/client/Microsoft.Identity.Client/Cache/Prototype/IdentityCacheWrapper.cs @@ -17,7 +17,8 @@ internal class IdentityCacheWrapper private static IIdentityLogger _identityLogger; private static readonly Lazy s_defaultIIdentityCache = new Lazy( () => CreateDefaultCache()); - private const string CategoryName = "tokens"; + 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. @@ -49,23 +50,44 @@ private static IIdentityCache CreateDefaultCache() { MaxNumberOfItemsForCategory = new Dictionary() { - { CategoryName, s_cacheOptions.SizeLimit }, + { AppTokensCategory, s_cacheOptions.AppTokenCacheSizeLimit }, + { UserTokensCategory, s_cacheOptions.UserTokenCacheSizeLimit }, } }; return new IdentityCachePrototype(memoryCacheOptions, _identityLogger, null); } - internal async Task GetAsync(string key) where T : ICacheObject, new() + internal async Task GetAppCacheAsync(string key) where T : ICacheObject, new() { - var entry = await _identityCache.GetAsync(CategoryName, key).ConfigureAwait(false); + 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 SetAsync(string key, T value, DateTimeOffset? cacheExpiry) where T : ICacheObject, new() + 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(CategoryName, key, value, new CacheEntryOptions(expirationTimeRelativeToNow, 1)).ConfigureAwait(false); + await _identityCache.SetAsync(category, key, value, new CacheEntryOptions(expirationTimeRelativeToNow, 1)).ConfigureAwait(false); } } } diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index 79f8da9437..98633a567b 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -232,11 +232,11 @@ async Task> IToke DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, logger); if (accessor is InMemoryPartitionedAppTokenCacheAccessor) { - await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + await IdentityCacheWrapper.SetAppCacheAsync(suggestedWebCacheKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); } else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) { - await IdentityCacheWrapper.SetAsync(suggestedWebCacheKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + await IdentityCacheWrapper.SetUserCacheAsync(suggestedWebCacheKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); } } @@ -1320,11 +1320,11 @@ bool ITokenCacheInternal.HasTokensNoLocks() DateTimeOffset? cacheExpiry = CalculateSuggestedCacheExpiry(accessor, requestContext.Logger); if (accessor is InMemoryPartitionedAppTokenCacheAccessor) { - await IdentityCacheWrapper.SetAsync(partitionKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + await IdentityCacheWrapper.SetAppCacheAsync(partitionKey, (InMemoryPartitionedAppTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); } else if (accessor is InMemoryPartitionedUserTokenCacheAccessor) { - await IdentityCacheWrapper.SetAsync(partitionKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); + await IdentityCacheWrapper.SetUserCacheAsync(partitionKey, (InMemoryPartitionedUserTokenCacheAccessor)accessor, cacheExpiry).ConfigureAwait(false); } } } @@ -1344,11 +1344,11 @@ internal async Task GetOrCreateAccessorAsync(string partiti ITokenCacheAccessor cachedAccessor; if (IsAppTokenCache) { - cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); + cachedAccessor = await IdentityCacheWrapper.GetAppCacheAsync(partitionKey).ConfigureAwait(false); } else { - cachedAccessor = await IdentityCacheWrapper.GetAsync(partitionKey).ConfigureAwait(false); + cachedAccessor = await IdentityCacheWrapper.GetUserCacheAsync(partitionKey).ConfigureAwait(false); } if (cachedAccessor == null) diff --git a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs index d150d8ed1e..a9a6e0f6e2 100644 --- a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForClientCacheTests.cs @@ -63,7 +63,7 @@ public async Task GlobalSetupAsync() if (!EnableCacheSerialization) { - builder.WithCacheOptions(new CacheOptions(CacheSize.TotalTenants)); + builder.WithCacheOptions(new CacheOptions(CacheSize.TotalTenants, 0)); } _cca = builder.BuildConcrete(); @@ -146,7 +146,7 @@ private async Task PopulateAppCacheAsync(ConfidentialClientApplication cca, int } else { - await (cca.AppTokenCache as TokenCache).IdentityCacheWrapper.SetAsync(key, (InMemoryPartitionedAppTokenCacheAccessor)accessor, null).ConfigureAwait(false); + 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 d8be1ae28e..a6092b04de 100644 --- a/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs +++ b/tests/Microsoft.Identity.Test.Performance/AcquireTokenForOboCacheTests.cs @@ -70,7 +70,7 @@ public async Task GlobalSetupAsync() if (!EnableCacheSerialization) { - builder.WithCacheOptions(new CacheOptions(CacheSize.TotalUsers)); + builder.WithCacheOptions(new CacheOptions(0, CacheSize.TotalUsers)); } _cca = builder.BuildConcrete(); @@ -169,7 +169,7 @@ private async Task PopulateUserCacheAsync(int totalUsers, int tokensPerUser, boo } else { - await (_cca.UserTokenCache as TokenCache).IdentityCacheWrapper.SetAsync(userAssertionHash, (InMemoryPartitionedUserTokenCacheAccessor)accessor, null).ConfigureAwait(false); + await (_cca.UserTokenCache as TokenCache).IdentityCacheWrapper.SetUserCacheAsync(userAssertionHash, (InMemoryPartitionedUserTokenCacheAccessor)accessor, null).ConfigureAwait(false); } } }