Skip to content

Commit 6ff1aa7

Browse files
trwalketrwalkegladjohn
authored
Cache key extensibility for MSAL (#5107)
* Adding cache logic for additional components * Adding logging. Adding tests. * Resolving test * Clean Up * Resolving cache key issues. Adding test cases. * Updating public api * Remove unused using directive * Adding credential type test * Updating public api files * Apply suggestions from code review Co-authored-by: Gladwin Johnson <[email protected]> * Update tests/Microsoft.Identity.Test.Unit/PublicApiTests/CacheKeyExtensionTests.cs Co-authored-by: Gladwin Johnson <[email protected]> * Addressing feedback. clean up. refactoring * Adding pop test Refactoring Clean up * Adding additional test * Updating validation checks Adding tests --------- Co-authored-by: trwalke <[email protected]> Co-authored-by: Gladwin Johnson <[email protected]>
1 parent e682cd1 commit 6ff1aa7

19 files changed

+669
-25
lines changed

src/client/Microsoft.Identity.Client/ApiConfig/AbstractConfidentialClientAcquireTokenParameterBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public T WithProofOfPossession(PoPAuthenticationConfiguration popAuthenticationC
9292
/// <summary>
9393
/// Modifies the request to acquire a Signed HTTP Request (SHR) Proof-of-Possession (PoP) token, rather than a Bearer.
9494
/// SHR PoP tokens are bound to the HTTP request and to a cryptographic key, which MSAL manages on Windows.
95-
/// SHR PoP tokens are different from mTLS PoP tokens, which are used for Mutual TLS (mTLS) authentication. See <see href="https://aka.ms/mtls-pop"/> for details.
95+
/// SHR PoP tokens are different from mTLS PoP tokens, which are used for Mutual TLS (mTLS) authentication. See <see href="https://aka.ms/mtls-pop"/> for details.
9696
/// </summary>
9797
/// <param name="popAuthenticationConfiguration">Configuration properties used to construct a Proof-of-Possession request.</param>
9898
/// <returns>The builder.</returns>

src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

44
using System;

src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ internal class AcquireTokenCommonParameters
3030
public Func<OnBeforeTokenRequestData, Task> OnBeforeTokenRequestHandler { get; internal set; }
3131
public X509Certificate2 MtlsCertificate { get; internal set; }
3232
public List<string> AdditionalCacheParameters { get; set; }
33+
public SortedList<string, string> CacheKeyComponents { get; internal set; }
3334
}
3435
}

src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ public X509Certificate2 ClientCredentialCertificate
160160
return null;
161161
}
162162
}
163+
164+
public SortedList<string, string> CacheKeyComponents { get; internal set; }
163165
#endregion
164166

165167
#region Region

src/client/Microsoft.Identity.Client/Cache/CacheKeyFactory.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System.Globalization;
4+
using System.Collections.Generic;
5+
using System.Linq;
56
using Microsoft.Identity.Client.Cache.Items;
67
using Microsoft.Identity.Client.Internal.Requests;
78
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
9+
using Microsoft.Identity.Client.Utils;
810

911
namespace Microsoft.Identity.Client.Cache
1012
{
@@ -48,12 +50,15 @@ public static string GetExternalCacheKeyFromResponse(
4850
return key;
4951
}
5052

51-
if (requestParameters.AppConfig.IsConfidentialClient ||
52-
requestParameters.ApiId == ApiEvent.ApiIds.AcquireTokenSilent)
53+
if (requestParameters.AppConfig.IsConfidentialClient)
5354
{
5455
return homeAccountIdFromResponse;
5556
}
56-
57+
if (requestParameters.ApiId == ApiEvent.ApiIds.AcquireTokenSilent)
58+
{
59+
return homeAccountIdFromResponse;
60+
}
61+
5762
return null;
5863
}
5964

@@ -78,17 +83,25 @@ private static bool GetOboOrAppKey(AuthenticationRequestParameters requestParame
7883
requestParameters.ApiId == ApiEvent.ApiIds.AcquireTokenForUserAssignedManagedIdentity)
7984
{
8085
string tenantId = requestParameters.Authority.TenantId ?? "";
81-
key = GetClientCredentialKey(requestParameters.AppConfig.ClientId, tenantId, requestParameters.AuthenticationScheme?.KeyId);
82-
86+
key = GetAppTokenCacheItemKey(requestParameters.AppConfig.ClientId, tenantId, requestParameters.AuthenticationScheme?.KeyId, requestParameters.CacheKeyComponents);
8387
return true;
8488
}
8589

8690
key = null;
8791
return false;
8892
}
8993

90-
public static string GetClientCredentialKey(string clientId, string tenantId, string popKid)
94+
public static string GetAppTokenCacheItemKey(
95+
string clientId,
96+
string tenantId,
97+
string popKid,
98+
SortedList<string, string> cacheKeyComponents = null)
9199
{
100+
if (cacheKeyComponents != null && cacheKeyComponents.Any())
101+
{
102+
return $"{popKid}{clientId}_{tenantId}_{CoreHelpers.ComputeAccessTokenExtCacheKey(cacheKeyComponents)}_AppTokenCache";
103+
}
104+
92105
return $"{popKid}{clientId}_{tenantId}_AppTokenCache";
93106
}
94107

src/client/Microsoft.Identity.Client/Cache/Items/MsalAccessTokenCacheItem.cs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections;
56
using System.Collections.Generic;
67
using System.Linq;
8+
using System.Security.Cryptography;
9+
using System.Text;
710
using Microsoft.Identity.Client.AuthScheme;
811
using Microsoft.Identity.Client.Cache.Keys;
912
using Microsoft.Identity.Client.Internal;
1013
using Microsoft.Identity.Client.OAuth2;
1114
using Microsoft.Identity.Client.Utils;
15+
1216
#if SUPPORTS_SYSTEM_TEXT_JSON
17+
using System.Text.Json.Nodes;
1318
using System.Text.Json;
1419
using JObject = System.Text.Json.Nodes.JsonObject;
1520
#else
@@ -31,7 +36,8 @@ internal MsalAccessTokenCacheItem(
3136
string homeAccountId,
3237
string keyId = null,
3338
string oboCacheKey = null,
34-
IEnumerable<string> persistedCacheParameters = null)
39+
IEnumerable<string> persistedCacheParameters = null,
40+
SortedList<string, string> cacheKeyComponents = null)
3541
: this(
3642
scopes: ScopeHelper.OrderScopesAlphabetically(response.Scope), // order scopes to avoid cache duplication. This is not in the hot path.
3743
cachedAt: DateTimeOffset.UtcNow,
@@ -48,6 +54,8 @@ internal MsalAccessTokenCacheItem(
4854
RawClientInfo = response.ClientInfo;
4955
HomeAccountId = homeAccountId;
5056
OboCacheKey = oboCacheKey;
57+
58+
InitializeAdditionalCacheKeyComponents(cacheKeyComponents);
5159
#if !MOBILE
5260
PersistedCacheParameters = AcquireCacheParametersFromResponse(persistedCacheParameters, response.ExtensionData);
5361
#endif
@@ -95,7 +103,8 @@ private IDictionary<string, string> AcquireCacheParametersFromResponse(
95103
string keyId = null,
96104
DateTimeOffset? refreshOn = null,
97105
string tokenType = StorageJsonValues.TokenTypeBearer,
98-
string oboCacheKey = null)
106+
string oboCacheKey = null,
107+
SortedList<string, string> cacheKeyComponents = null)
99108
: this(scopes, cachedAt, expiresOn, extendedExpiresOn, refreshOn, tenantId, keyId, tokenType)
100109
{
101110
Environment = preferredCacheEnv;
@@ -105,6 +114,8 @@ private IDictionary<string, string> AcquireCacheParametersFromResponse(
105114
HomeAccountId = homeAccountId;
106115
OboCacheKey = oboCacheKey;
107116

117+
InitializeAdditionalCacheKeyComponents(cacheKeyComponents);
118+
108119
InitCacheKey();
109120
}
110121

@@ -153,11 +164,21 @@ internal MsalAccessTokenCacheItem WithExpiresOn(DateTimeOffset expiresOn)
153164
KeyId,
154165
RefreshOn,
155166
TokenType,
156-
OboCacheKey);
167+
OboCacheKey,
168+
AdditionalCacheKeyComponents);
157169

158170
return newAtItem;
159171
}
160172

173+
private void InitializeAdditionalCacheKeyComponents(SortedList<string, string> cacheKeyComponents)
174+
{
175+
if (cacheKeyComponents != null && cacheKeyComponents.Any())
176+
{
177+
AdditionalCacheKeyComponents = cacheKeyComponents;
178+
CredentialType = StorageJsonValues.CredentialTypeAccessTokenExtended;
179+
}
180+
}
181+
161182
//internal for test
162183
internal void InitCacheKey()
163184
{
@@ -170,6 +191,19 @@ internal void InitCacheKey()
170191
_credentialDescriptor = StorageJsonValues.CredentialTypeAccessTokenWithAuthScheme;
171192
}
172193

194+
if (AdditionalCacheKeyComponents != null)
195+
{
196+
_credentialDescriptor = StorageJsonValues.CredentialTypeAccessTokenExtended;
197+
if (_extraKeyParts != null)
198+
{
199+
_extraKeyParts = _extraKeyParts.Concat(new[] { CoreHelpers.ComputeAccessTokenExtCacheKey(AdditionalCacheKeyComponents) }).ToArray();
200+
}
201+
else
202+
{
203+
_extraKeyParts = new[] { CoreHelpers.ComputeAccessTokenExtCacheKey(AdditionalCacheKeyComponents) };
204+
}
205+
}
206+
173207
CacheKey = MsalCacheKeys.GetCredentialKey(
174208
HomeAccountId,
175209
Environment,
@@ -246,6 +280,8 @@ internal string TenantId
246280

247281
internal string CacheKey { get; private set; }
248282

283+
internal SortedList<string, string> AdditionalCacheKeyComponents { get; private set; }
284+
249285
/// <summary>
250286
/// Additional parameters that were requested in the token request and are stored in the cache.
251287
/// These are acquired from the response and are stored in the cache for later use.
@@ -284,6 +320,7 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
284320
string keyId = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.KeyId);
285321
string tokenType = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.TokenType) ?? StorageJsonValues.TokenTypeBearer;
286322
string scopes = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target);
323+
var additionalCacheKeyComponents = JsonHelper.ExtractInnerJsonAsDictionary(j, StorageJsonKeys.CacheExtensions);
287324

288325
var item = new MsalAccessTokenCacheItem(
289326
scopes: scopes,
@@ -295,6 +332,12 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
295332
keyId: keyId,
296333
tokenType: tokenType);
297334

335+
if (additionalCacheKeyComponents != null)
336+
{
337+
item.AdditionalCacheKeyComponents = new SortedList<string, string>(additionalCacheKeyComponents);
338+
item.CredentialType = StorageJsonValues.CredentialTypeAccessTokenExtended;
339+
}
340+
298341
item.OboCacheKey = oboCacheKey;
299342
item.PopulateFieldsFromJObject(j);
300343

@@ -324,7 +367,21 @@ internal override JObject ToJObject()
324367
// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
325368
// this is here for back compatibility
326369
SetItemIfValueNotNull(json, StorageJsonKeys.ExtendedExpiresOn_MsalCompat, extExpiresUnixTimestamp);
370+
if (AdditionalCacheKeyComponents != null)
371+
{
372+
#if SUPPORTS_SYSTEM_TEXT_JSON
373+
var obj = new JsonObject();
374+
375+
foreach (KeyValuePair<string, string> accId in AdditionalCacheKeyComponents)
376+
{
377+
obj[accId.Key] = accId.Value;
378+
}
327379

380+
json[StorageJsonKeys.CacheExtensions] = obj;
381+
#else
382+
SetItemIfValueNotNull(json, StorageJsonKeys.CacheExtensions, JObject.FromObject(AdditionalCacheKeyComponents));
383+
#endif
384+
}
328385
return json;
329386
}
330387

src/client/Microsoft.Identity.Client/Cache/StorageJsonKeys.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Generic;
5+
46
namespace Microsoft.Identity.Client.Cache
57
{
68
internal static class StorageJsonKeys
@@ -39,5 +41,47 @@ internal static class StorageJsonKeys
3941
// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
4042
// this is here for back compatibility
4143
public const string ExtendedExpiresOn_MsalCompat = "ext_expires_on";
44+
45+
public const string CacheExtensions = "ext";
46+
47+
//Known storeage keys need to be added here
48+
public static readonly HashSet<string> s_knownStorageJsonKeys = new HashSet<string>
49+
{
50+
HomeAccountId,
51+
Environment,
52+
Realm,
53+
LocalAccountId,
54+
Username,
55+
AuthorityType,
56+
AlternativeAccountId,
57+
GivenName,
58+
FamilyName,
59+
MiddleName,
60+
Name,
61+
AvatarUrl,
62+
CredentialType,
63+
ClientId,
64+
Secret,
65+
Target,
66+
CachedAt,
67+
ExpiresOn,
68+
RefreshOn,
69+
ExtendedExpiresOn,
70+
ClientInfo,
71+
FamilyId,
72+
AppMetadata,
73+
KeyId,
74+
TokenType,
75+
WamAccountIds,
76+
AccountSource,
77+
UserAssertionHash,
78+
ExtendedExpiresOn_MsalCompat,
79+
CacheExtensions
80+
};
81+
82+
public static bool IsKnownStorageJsonKey(string key)
83+
{
84+
return s_knownStorageJsonKeys.Contains(key);
85+
}
4286
}
4387
}

src/client/Microsoft.Identity.Client/Cache/StorageJsonValues.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal static class StorageJsonValues
1212
public const string TokenTypeBearer = "Bearer";
1313
public const string CredentialTypeRefreshToken = "RefreshToken";
1414
public const string CredentialTypeAccessToken = "AccessToken";
15+
public const string CredentialTypeAccessTokenExtended = "ATExt";
1516
public const string CredentialTypeAccessTokenWithAuthScheme = "AccessToken_With_AuthScheme";
1617
public const string CredentialTypeIdToken = "IdToken";
1718
public const string AccountRootKey = "Account";

src/client/Microsoft.Identity.Client/Extensibility/AcquireTokenForClientBuilderExtensions.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.ComponentModel;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Text;
9+
using Microsoft.Identity.Client.Cache;
610

711
namespace Microsoft.Identity.Client.Extensibility
812
{
@@ -11,6 +15,60 @@ namespace Microsoft.Identity.Client.Extensibility
1115
/// </summary>
1216
public static class AcquireTokenForClientBuilderExtensions
1317
{
18+
/// <summary>
19+
/// Specifies additional cache key components to use when caching and retrieving tokens.
20+
/// </summary>
21+
/// <param name="cacheKeyComponents">The list of additional cache key components.</param>
22+
/// <param name="builder"></param>
23+
/// <returns>The builder.</returns>
24+
/// <remarks>
25+
/// <list type="bullet">
26+
/// <item><description>This api can be used to associate certificate key identifiers along with other keys with a particular token.</description></item>
27+
/// <item><description>In order for the tokens to be successfully retrieved from the cache, all components used to cache the token must be provided.</description></item>
28+
/// </list>
29+
/// </remarks>
30+
internal static AcquireTokenForClientParameterBuilder WithAdditionalCacheKeyComponents(this AcquireTokenForClientParameterBuilder builder,
31+
IDictionary<string, string> cacheKeyComponents)
32+
{
33+
builder.ValidateUseOfExperimentalFeature();
34+
35+
if (cacheKeyComponents == null || cacheKeyComponents.Count == 0)
36+
{
37+
//no-op
38+
return builder;
39+
}
40+
41+
StringBuilder offendingKeys = new();
42+
43+
//Ensure known JSON keys are not added to cache key components
44+
foreach (var kvp in cacheKeyComponents)
45+
{
46+
if (StorageJsonKeys.IsKnownStorageJsonKey(kvp.Key))
47+
{
48+
offendingKeys.AppendLine(kvp.Key);
49+
}
50+
}
51+
52+
if (offendingKeys.Length != 0)
53+
{
54+
throw new ArgumentException($"Keys added to {nameof(cacheKeyComponents)} are invalid. Offending keys are: {offendingKeys.ToString()}");
55+
}
56+
57+
if (builder.CommonParameters.CacheKeyComponents == null)
58+
{
59+
builder.CommonParameters.CacheKeyComponents = new SortedList<string, string>(cacheKeyComponents);
60+
}
61+
else
62+
{
63+
foreach (var kvp in cacheKeyComponents)
64+
{
65+
builder.CommonParameters.CacheKeyComponents.Add(kvp.Key, kvp.Value);
66+
}
67+
}
68+
69+
return builder;
70+
}
71+
1472
/// <summary>
1573
/// Binds the token to a key in the cache. L2 cache keys contain the key id.
1674
/// No cryptographic operations is performed on the token.

src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public string Claims
127127

128128
public IEnumerable<string> PersistedCacheParameters => _commonParameters.AdditionalCacheParameters;
129129

130+
public SortedList<string, string> CacheKeyComponents => _commonParameters.CacheKeyComponents;
131+
130132
#region TODO REMOVE FROM HERE AND USE FROM SPECIFIC REQUEST PARAMETERS
131133
// TODO: ideally, these can come from the particular request instance and not be in RequestBase since it's not valid for all requests.
132134

0 commit comments

Comments
 (0)