Skip to content

Commit a8c9424

Browse files
pr comments
1 parent d45a0ac commit a8c9424

File tree

10 files changed

+130
-53
lines changed

10 files changed

+130
-53
lines changed

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

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ namespace Microsoft.Identity.Client
2929
public sealed class AcquireTokenForClientParameterBuilder :
3030
AbstractConfidentialClientAcquireTokenParameterBuilder<AcquireTokenForClientParameterBuilder>
3131
{
32-
private AcquireTokenForClientParameters Parameters { get; } = new AcquireTokenForClientParameters();
32+
internal AcquireTokenForClientParameters Parameters { get; } = new AcquireTokenForClientParameters();
3333

3434
/// <inheritdoc/>
3535
internal AcquireTokenForClientParameterBuilder(IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor)
@@ -158,26 +158,6 @@ public AcquireTokenForClientParameterBuilder WithFmiPath(string pathSuffix)
158158
return this;
159159
}
160160

161-
/// <summary>
162-
/// Specifies that the token refresh should only occur if the specified SHA-256 hash
163-
/// of the previously issued access token matches a cached token.
164-
/// If the hash does not match, the existing cached token is returned without refresh.
165-
/// </summary>
166-
/// <param name="hash">The SHA-256 hash of the access token to refresh.</param>
167-
/// <returns>The builder to chain the .With methods</returns>
168-
public AcquireTokenForClientParameterBuilder WithAccessTokenSha256ToRefresh(string hash)
169-
{
170-
ValidateUseOfExperimentalFeature();
171-
172-
if (string.IsNullOrWhiteSpace(hash))
173-
{
174-
throw new ArgumentNullException(nameof(hash), "Access token hash cannot be null or empty.");
175-
}
176-
177-
Parameters.AccessTokenHashToRefresh = hash;
178-
return this;
179-
}
180-
181161
/// <inheritdoc/>
182162
internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationToken cancellationToken)
183163
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Microsoft.Identity.Client;
6+
using Microsoft.Identity.Client.ApiConfig.Parameters;
7+
8+
namespace Microsoft.Identity.Client.RP
9+
{
10+
/// <summary>
11+
/// Resource Provider extensibility methods for AcquireTokenForClientParameterBuilder
12+
/// </summary>
13+
public static class AcquireTokenForClientParameterBuilderForResourceProviders
14+
{
15+
/// <summary>
16+
/// Configures the SDK to not retrieve a token from the cache if it matches the SHA256 hash
17+
/// of the token configured. Similar to WithForceRefresh(bool) API, but instead of bypassing
18+
/// the cache for all tokens, the cache bypass only occurs for 1 token
19+
/// </summary>
20+
/// <param name="builder">The existing AcquireTokenForClientParameterBuilder instance.</param>
21+
/// <param name="hash">
22+
/// A Base64-encoded SHA-256 hash of the token (UTF-8). For example:
23+
/// <c>Convert.ToBase64String(SHA256(Encoding.UTF8.GetBytes(accessToken)))</c>.
24+
/// </param>
25+
/// <returns>The builder to chain the .With methods.</returns>
26+
public static AcquireTokenForClientParameterBuilder WithAccessTokenSha256ToRefresh(
27+
this AcquireTokenForClientParameterBuilder builder,
28+
string hash)
29+
{
30+
if (!string.IsNullOrWhiteSpace(hash))
31+
{
32+
builder.Parameters.AccessTokenHashToRefresh = hash;
33+
}
34+
35+
return builder;
36+
}
37+
}
38+
}

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

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Security.Cryptography;
77
using System.Text;
8+
using System.Text.RegularExpressions;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.Identity.Client.ApiConfig.Parameters;
@@ -13,6 +14,7 @@
1314
using Microsoft.Identity.Client.Extensibility;
1415
using Microsoft.Identity.Client.Instance;
1516
using Microsoft.Identity.Client.OAuth2;
17+
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
1618
using Microsoft.Identity.Client.Utils;
1719

1820
namespace Microsoft.Identity.Client.Internal.Requests
@@ -21,6 +23,7 @@ internal class ClientCredentialRequest : RequestBase
2123
{
2224
private readonly AcquireTokenForClientParameters _clientParameters;
2325
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1);
26+
private readonly ICryptographyManager _cryptoManager;
2427

2528
public ClientCredentialRequest(
2629
IServiceBundle serviceBundle,
@@ -29,6 +32,7 @@ public ClientCredentialRequest(
2932
: base(serviceBundle, authenticationRequestParameters, clientParameters)
3033
{
3134
_clientParameters = clientParameters;
35+
_cryptoManager = serviceBundle.PlatformProxy.CryptographyManager;
3236
}
3337

3438
protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
@@ -47,14 +51,20 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
4751
{
4852
logger.Error(MsalErrorMessage.ClientCredentialWrongAuthority);
4953
}
50-
5154
AuthenticationResult authResult;
5255

56+
// Skip cache if either:
57+
// 1) ForceRefresh is set, or
58+
// 2) Claims are specified and there is no AccessTokenHashToRefresh.
59+
// This ensures that when both claims and AccessTokenHashToRefresh are set,
60+
// we do NOT skip the cache, allowing MSAL to attempt retrieving a matching
61+
// cached token by the provided hash before requesting a new token.
5362
bool skipCache = _clientParameters.ForceRefresh ||
5463
(!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims) &&
5564
string.IsNullOrEmpty(_clientParameters.AccessTokenHashToRefresh));
5665

57-
// Skip checking cache when force refresh or claims are specified
66+
// Skip checking cache when either ForceRefresh is true
67+
// or (Claims are present without a token hash).
5868
if (skipCache)
5969
{
6070
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims;
@@ -196,42 +206,83 @@ private async Task<AuthenticationResult> SendTokenRequestToAppTokenProviderAsync
196206

197207
private async Task<MsalAccessTokenCacheItem> GetCachedAccessTokenAsync()
198208
{
209+
// 1) Get the cached item
199210
MsalAccessTokenCacheItem cachedAccessTokenItem = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false);
200211

201-
if (cachedAccessTokenItem != null && !_clientParameters.ForceRefresh)
212+
// 2) If no cached item or force refresh is requested, return null
213+
if (!IsValidCachedToken(cachedAccessTokenItem))
202214
{
203-
if (!string.IsNullOrEmpty(_clientParameters.AccessTokenHashToRefresh))
204-
{
205-
string cachedTokenHash = ComputeSHA256(cachedAccessTokenItem.Secret);
215+
return null;
216+
}
206217

207-
if (string.Equals(cachedTokenHash, _clientParameters.AccessTokenHashToRefresh, StringComparison.Ordinal))
208-
{
209-
AuthenticationRequestParameters.RequestContext.Logger.Info(
210-
"[ClientCredentialRequest] A cached token was found, but it matches the AccessTokenHashToRefresh, so it is ignored");
211-
return null; // triggers a new token acquisition
212-
}
213-
}
218+
// 3) If there is a matching AccessTokenHashToRefresh, ignore this cached token
219+
if (IsTokenIgnoredByMatchingHash(cachedAccessTokenItem))
220+
{
221+
return null;
222+
}
223+
224+
// 4) Otherwise, record a cache hit and return the cached token
225+
MarkAccessTokenAsCacheHit();
226+
return cachedAccessTokenItem;
227+
}
214228

215-
AuthenticationRequestParameters.RequestContext.ApiEvent.IsAccessTokenCacheHit = true;
216-
Metrics.IncrementTotalAccessTokensFromCache();
217-
return cachedAccessTokenItem;
229+
/// <summary>
230+
/// Checks if the cached access token can be used.
231+
/// </summary>
232+
private bool IsValidCachedToken(MsalAccessTokenCacheItem cachedAccessTokenItem)
233+
{
234+
// Return false if:
235+
// - The cache is empty
236+
// - ForceRefresh is enabled
237+
if (cachedAccessTokenItem == null || _clientParameters.ForceRefresh)
238+
{
239+
return false;
218240
}
219241

220-
return null;
242+
return true;
221243
}
222244

223-
private static string ComputeSHA256(string token)
245+
/// <summary>
246+
/// Determines whether the cached token should be ignored due to matching the AccessTokenHashToRefresh.
247+
/// </summary>
248+
private bool IsTokenIgnoredByMatchingHash(MsalAccessTokenCacheItem cachedAccessTokenItem)
224249
{
225-
#if NET6_0_OR_GREATER
226-
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
227-
return Convert.ToBase64String(hashBytes);
228-
#else
229-
using (var sha256 = SHA256.Create())
250+
if (string.IsNullOrEmpty(_clientParameters.AccessTokenHashToRefresh))
251+
{
252+
return false;
253+
}
254+
255+
string cachedTokenHash = _cryptoManager.CreateSha256Hash(cachedAccessTokenItem.Secret);
256+
257+
// If the hash of the cached token matches the hash to refresh, ignore the cached token
258+
bool matchesHash = string.Equals(
259+
cachedTokenHash,
260+
_clientParameters.AccessTokenHashToRefresh,
261+
StringComparison.Ordinal);
262+
263+
if (matchesHash)
230264
{
231-
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
232-
return Convert.ToBase64String(hashBytes);
265+
AuthenticationRequestParameters.RequestContext.Logger.Info(
266+
"[ClientCredentialRequest] A cached token was found and its hash matches AccessTokenHashToRefresh, so it is ignored.");
267+
return true;
233268
}
234-
#endif
269+
else
270+
{
271+
AuthenticationRequestParameters.RequestContext.Logger.Info(
272+
"[ClientCredentialRequest] A cached token was found, but its hash does NOT match AccessTokenHashToRefresh. Using the cached token.");
273+
return false;
274+
}
275+
}
276+
277+
/// <summary>
278+
/// Marks the current access token retrieval as a successful cache hit,
279+
/// and increments any relevant telemetry or counters.
280+
/// </summary>
281+
private void MarkAccessTokenAsCacheHit()
282+
{
283+
// Mark the request as a cache hit
284+
AuthenticationRequestParameters.RequestContext.ApiEvent.IsAccessTokenCacheHit = true;
285+
Metrics.IncrementTotalAccessTokensFromCache();
235286
}
236287

237288
private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem)
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
const Microsoft.Identity.Client.MsalError.ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible" -> string
2-
Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAccessTokenSha256ToRefresh(string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder
2+
Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders
3+
static Microsoft.Identity.Client.RP.AcquireTokenForClientParameterBuilderForResourceProviders.WithAccessTokenSha256ToRefresh(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder builder, string hash) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder

tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Threading;
1212
using System.Threading.Tasks;
1313
using Microsoft.Identity.Client;
14+
using Microsoft.Identity.Client.RP;
1415
using Microsoft.Identity.Client.Cache;
1516
using Microsoft.Identity.Client.Internal;
1617
using Microsoft.Identity.Client.Internal.ClientCredential;
@@ -2338,6 +2339,7 @@ public async Task AcquireTokenForClient_WithClaims_And_MismatchedHash_UsesCache_
23382339

23392340
// First network call: populates the cache with "cache-token"
23402341
httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(token: "cache-token");
2342+
23412343
var initialResult = await app.AcquireTokenForClient(TestConstants.s_scope)
23422344
.ExecuteAsync()
23432345
.ConfigureAwait(false);

0 commit comments

Comments
 (0)