Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add partitioned user cache #2881

Merged
merged 5 commits into from
Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Executors;
Expand Down Expand Up @@ -63,7 +62,7 @@ private AcquireTokenSilentParameterBuilder WithLoginHint(string loginHint)
/// <summary>
/// Specifies if the client application should force refreshing the
/// token from the user token cache. By default the token is taken from the
/// the application token cache (forceRefresh=false)
/// the user token cache (forceRefresh=false)
/// </summary>
/// <param name="forceRefresh">If <c>true</c>, ignore any access token in the user token cache
/// and attempt to acquire new access token using the refresh token for the account
Expand Down
132 changes: 132 additions & 0 deletions src/client/Microsoft.Identity.Client/Cache/CacheKeyFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Cache.Keys;
using Microsoft.Identity.Client.Internal.Requests;

namespace Microsoft.Identity.Client.Cache
{
/// <summary>
/// Responsible for computing:
/// - external distributed cache key (from request and responses)
/// - internal cache partition keys (as above, but also from cache items)
///
/// These are the same string, but MSAL cannot control if the app developer actually uses distributed caching.
/// However, MSAL's in-memory cache needs to be partitioned, and this class computes the partition key.
/// </summary>
internal static class CacheKeyFactory
{
public static string GetKeyFromRequest(AuthenticationRequestParameters requestParameters)
{
if (GetOboOrAppKey(requestParameters, out string key))
{
return key;
}

if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenSilent ||
requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.RemoveAccount)
{
return requestParameters.Account?.HomeAccountId?.Identifier;
}

if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.GetAccountById)
{
return requestParameters.HomeAccountId;
}

return null;
}

public static string GetExternalCacheKeyFromResponse(
AuthenticationRequestParameters requestParameters,
string homeAccountIdFromResponse)
{
if (GetOboOrAppKey(requestParameters, out string key))
{
return key;
}

if (requestParameters.IsConfidentialClient ||
requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenSilent)
{
return homeAccountIdFromResponse;
}

return null;
}

public static string GetInternalPartitionKeyFromResponse(
AuthenticationRequestParameters requestParameters,
string homeAccountIdFromResponse)
{
return GetExternalCacheKeyFromResponse(requestParameters, homeAccountIdFromResponse) ??
homeAccountIdFromResponse;
}

private static bool GetOboOrAppKey(AuthenticationRequestParameters requestParameters, out string key)
{
if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenOnBehalfOf)
{
key = requestParameters.UserAssertion.AssertionHash;
return true;
}

if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenForClient)
{
string tenantId = requestParameters.Authority.TenantId ?? "";
key = GetClientCredentialKey(requestParameters.AppConfig.ClientId, tenantId);

return true;
}

key = null;
return false;
}

public static string GetClientCredentialKey(string clientId, string tenantId)
{
return $"{clientId}_{tenantId}_AppTokenCache";
}

public static string GetKeyFromCachedItem(MsalAccessTokenCacheItem accessTokenCacheItem)
{
string partitionKey = !string.IsNullOrEmpty(accessTokenCacheItem.UserAssertionHash) ?
accessTokenCacheItem.UserAssertionHash :
accessTokenCacheItem.HomeAccountId;

return partitionKey;
}

public static string GetKeyFromCachedItem(MsalRefreshTokenCacheItem refreshTokenCacheItem)
{
string partitionKey = !string.IsNullOrEmpty(refreshTokenCacheItem.UserAssertionHash) ?
refreshTokenCacheItem.UserAssertionHash :
refreshTokenCacheItem.HomeAccountId;

return partitionKey;
}

// Id tokens are not indexed by OBO key, only by home account key
public static string GetIdTokenKeyFromCachedItem(MsalAccessTokenCacheItem accessTokenCacheItem)
{
return accessTokenCacheItem.HomeAccountId;
}

public static string GetKeyFromAccount(MsalAccountCacheKey accountKey)
{
return accountKey.HomeAccountId;
}

public static string GetKeyFromCachedItem(MsalIdTokenCacheItem idTokenCacheItem)
{
return idTokenCacheItem.HomeAccountId;
}

public static string GetKeyFromCachedItem(MsalAccountCacheItem accountCacheItem)
{
return accountCacheItem.HomeAccountId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ public async Task<IDictionary<string, TenantProfile>> GetTenantProfilesAsync(str
return await TokenCacheInternal.GetTenantProfilesAsync(_requestParams, homeAccountId).ConfigureAwait(false);
}

public async Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalIdTokenCacheKey idTokenCacheKey)
public async Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem accessTokenCacheItem)
{
await RefreshCacheForReadOperationsAsync(CacheEvent.TokenTypes.ID).ConfigureAwait(false);
return TokenCacheInternal.GetIdTokenCacheItem(idTokenCacheKey);
return TokenCacheInternal.GetIdTokenCacheItem(accessTokenCacheItem);
}

public async Task<MsalRefreshTokenCacheItem> FindFamilyRefreshTokenAsync(string familyId)
Expand Down Expand Up @@ -101,7 +101,7 @@ public async Task<IEnumerable<IAccount>> GetAccountsAsync()
/// </remarks>
private async Task RefreshCacheForReadOperationsAsync(CacheEvent.TokenTypes cacheEventType)
{
if (TokenCacheInternal.IsTokenCacheSerialized())
if (TokenCacheInternal.IsAppSubscribedToSerializationEvents())
{
if (!_cacheRefreshedForRead)
{
Expand All @@ -121,7 +121,7 @@ private async Task RefreshCacheForReadOperationsAsync(CacheEvent.TokenTypes cach
{
using (_requestParams.RequestContext.CreateTelemetryHelper(cacheEvent))
{
string key = SuggestedWebCacheKeyFactory.GetKeyFromRequest(_requestParams);
string key = CacheKeyFactory.GetKeyFromRequest(_requestParams);

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ internal interface ICacheSessionManager
ITokenCacheInternal TokenCacheInternal { get; }
Task<MsalAccessTokenCacheItem> FindAccessTokenAsync();
Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> SaveTokenResponseAsync(MsalTokenResponse tokenResponse);
Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalIdTokenCacheKey idTokenCacheKey);
Task<MsalIdTokenCacheItem> GetIdTokenCacheItemAsync(MsalAccessTokenCacheItem accessTokenCacheItem);
Task<MsalRefreshTokenCacheItem> FindRefreshTokenAsync();
Task<MsalRefreshTokenCacheItem> FindFamilyRefreshTokenAsync(string familyId);
Task<bool?> IsAppFociMemberAsync(string familyId);

Task<IEnumerable<IAccount>> GetAccountsAsync();
Task<IDictionary<string, TenantProfile>> GetTenantProfilesAsync(string homeAccountId);


}
}
65 changes: 48 additions & 17 deletions src/client/Microsoft.Identity.Client/Cache/ITokenCacheAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,63 @@ internal interface ITokenCacheAccessor

void SaveAppMetadata(MsalAppMetadataCacheItem item);

MsalAccessTokenCacheItem GetAccessToken(MsalAccessTokenCacheKey accessTokenKey);

MsalRefreshTokenCacheItem GetRefreshToken(MsalRefreshTokenCacheKey refreshTokenKey);

MsalIdTokenCacheItem GetIdToken(MsalIdTokenCacheKey idTokenKey);
MsalIdTokenCacheItem GetIdToken(MsalAccessTokenCacheItem accessTokenCacheItem);

MsalAccountCacheItem GetAccount(MsalAccountCacheKey accountKey);

MsalAppMetadataCacheItem GetAppMetadata(MsalAppMetadataCacheKey appMetadataKey);

void DeleteAccessToken(MsalAccessTokenCacheKey cacheKey);
void DeleteAccessToken(MsalAccessTokenCacheItem item);

void DeleteRefreshToken(MsalRefreshTokenCacheKey cacheKey);
void DeleteRefreshToken(MsalRefreshTokenCacheItem item);

void DeleteIdToken(MsalIdTokenCacheKey cacheKey);
void DeleteIdToken(MsalIdTokenCacheItem item);

void DeleteAccount(MsalAccountCacheKey cacheKey);
void DeleteAccount(MsalAccountCacheItem item);

/// <summary>
/// Returns all access tokens from the underlying cache collection.
/// If optionalTenantIdFilter parameter is specified, returns access tokens pertaining to the specified tenant.
/// Token cache accessors implementing this interface are not required to obey Parameter optionalTenantIdFilter.
/// See <see cref="PlatformsCommon.Shared.InMemoryPartitionedTokenCacheAccessor.GetAllAccessTokens"/> which uses this filter.
/// See <see cref="PlatformsCommon.Shared.InMemoryTokenCacheAccessor.GetAllAccessTokens"/> which does not use this filter.
/// If <paramref name="optionalPartitionKey"/> is specified, returns access tokens from that partition only.
/// </summary>
IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string optionalTenantIdFilter = null);
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string optionalPartitionKey = null);

IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens();
/// <summary>
/// Returns all refresh tokens from the underlying cache collection.
/// If <paramref name="optionalPartitionKey"/> is specified, returns refresh tokens from that partition only.
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(string optionalPartitionKey = null);

IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens();
/// <summary>
/// Returns all ID tokens from the underlying cache collection.
/// If <paramref name="optionalPartitionKey"/> is specified, returns ID tokens from that partition only.
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string optionalPartitionKey = null);

IReadOnlyList<MsalAccountCacheItem> GetAllAccounts();
/// <summary>
/// Returns all accounts from the underlying cache collection.
/// If <paramref name="optionalPartitionKey"/> is specified, returns accounts from that partition only.
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccountCacheItem> GetAllAccounts(string optionalPartitionKey = null);

IReadOnlyList<MsalAppMetadataCacheItem> GetAllAppMetadata();

Expand All @@ -59,5 +84,11 @@ internal interface ITokenCacheAccessor
#endif

void Clear();

/// <remarks>
/// WARNING: this API is slow as it loads all tokens, not just from 1 partition.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// </remarks>
bool HasAccessOrRefreshTokens();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ internal MsalAccessTokenCacheItem(

HomeAccountId = homeAccountId;
AddJitterToTokenRefreshOn();
}
}

private string _tenantId;

Expand Down Expand Up @@ -133,7 +133,7 @@ internal DateTimeOffset? RefreshOn
get
{
return !string.IsNullOrEmpty(RefreshOnUnixTimestamp) ?
CoreHelpers.UnixTimestampStringToDateTime(RefreshOnUnixTimestamp):
CoreHelpers.UnixTimestampStringToDateTime(RefreshOnUnixTimestamp) :
(DateTimeOffset?)null;
}
}
Expand Down Expand Up @@ -178,12 +178,12 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)

var item = new MsalAccessTokenCacheItem(JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target))
{
TenantId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm),
TenantId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm),
CachedAt = cachedAt.ToString(CultureInfo.InvariantCulture),
ExpiresOnUnixTimestamp = expiresOn.ToString(CultureInfo.InvariantCulture),
ExtendedExpiresOnUnixTimestamp = extendedExpiresOn.ToString(CultureInfo.InvariantCulture),
RefreshOnUnixTimestamp = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.RefreshOn),
UserAssertionHash = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHash),
UserAssertionHash = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.UserAssertionHash),
KeyId = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.KeyId),
TokenType = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.TokenType) ?? StorageJsonValues.TokenTypeBearer
};
Expand Down Expand Up @@ -226,7 +226,7 @@ internal MsalAccessTokenCacheKey GetKey()
TenantId,
HomeAccountId,
ClientId,
_scopes,
_scopes,
TokenType);
}

Expand All @@ -240,5 +240,10 @@ internal bool NeedsRefresh()
return RefreshOn.HasValue &&
RefreshOn.Value < DateTime.UtcNow;
}

internal bool IsExpired()
{
return ExpiresOn < DateTime.UtcNow + Constants.AccessTokenExpirationBuffer;
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal class MsalAccessTokenCacheKey : IiOSKey
private readonly string[] _extraKeyParts;

internal string TenantId => _tenantId;
internal string ClientId => _clientId;
internal string HomeAccountId => _homeAccountId;

internal MsalAccessTokenCacheKey(
string environment,
Expand Down Expand Up @@ -66,7 +68,6 @@ internal MsalAccessTokenCacheKey(

public override string ToString()
{

return MsalCacheKeys.GetCredentialKey(
_homeAccountId,
_environment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ internal class MsalAccountCacheKey : IiOSKey
private readonly string _username;
private readonly string _authorityType;

internal string HomeAccountId => _homeAccountId;

public MsalAccountCacheKey(string environment, string tenantId, string userIdentifier, string username, string authorityType)
{
if (string.IsNullOrEmpty(environment))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal class MsalIdTokenCacheKey : IiOSKey
private readonly string _clientId;
private readonly string _tenantId;

internal string HomeAccountId => _homeAccountId;

public MsalIdTokenCacheKey(
string environment,
string tenantId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal class MsalRefreshTokenCacheKey : IiOSKey //TODO bogavril: add a base cl
private readonly string _clientId;
private readonly string _familyId;

internal string HomeAccountId => _homeAccountId;

/// <summary>
/// Constructor
/// </summary>
Expand Down
Loading