5
5
using System . Collections . Generic ;
6
6
using System . Security . Cryptography ;
7
7
using System . Text ;
8
+ using System . Text . RegularExpressions ;
8
9
using System . Threading ;
9
10
using System . Threading . Tasks ;
10
11
using Microsoft . Identity . Client . ApiConfig . Parameters ;
13
14
using Microsoft . Identity . Client . Extensibility ;
14
15
using Microsoft . Identity . Client . Instance ;
15
16
using Microsoft . Identity . Client . OAuth2 ;
17
+ using Microsoft . Identity . Client . PlatformsCommon . Interfaces ;
16
18
using Microsoft . Identity . Client . Utils ;
17
19
18
20
namespace Microsoft . Identity . Client . Internal . Requests
@@ -21,6 +23,7 @@ internal class ClientCredentialRequest : RequestBase
21
23
{
22
24
private readonly AcquireTokenForClientParameters _clientParameters ;
23
25
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim ( 1 , 1 ) ;
26
+ private readonly ICryptographyManager _cryptoManager ;
24
27
25
28
public ClientCredentialRequest (
26
29
IServiceBundle serviceBundle ,
@@ -29,6 +32,7 @@ public ClientCredentialRequest(
29
32
: base ( serviceBundle , authenticationRequestParameters , clientParameters )
30
33
{
31
34
_clientParameters = clientParameters ;
35
+ _cryptoManager = serviceBundle . PlatformProxy . CryptographyManager ;
32
36
}
33
37
34
38
protected override async Task < AuthenticationResult > ExecuteAsync ( CancellationToken cancellationToken )
@@ -47,14 +51,20 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
47
51
{
48
52
logger . Error ( MsalErrorMessage . ClientCredentialWrongAuthority ) ;
49
53
}
50
-
51
54
AuthenticationResult authResult ;
52
55
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.
53
62
bool skipCache = _clientParameters . ForceRefresh ||
54
63
( ! string . IsNullOrEmpty ( AuthenticationRequestParameters . Claims ) &&
55
64
string . IsNullOrEmpty ( _clientParameters . AccessTokenHashToRefresh ) ) ;
56
65
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).
58
68
if ( skipCache )
59
69
{
60
70
AuthenticationRequestParameters . RequestContext . ApiEvent . CacheInfo = CacheRefreshReason . ForceRefreshOrClaims ;
@@ -196,42 +206,83 @@ private async Task<AuthenticationResult> SendTokenRequestToAppTokenProviderAsync
196
206
197
207
private async Task < MsalAccessTokenCacheItem > GetCachedAccessTokenAsync ( )
198
208
{
209
+ // 1) Get the cached item
199
210
MsalAccessTokenCacheItem cachedAccessTokenItem = await CacheManager . FindAccessTokenAsync ( ) . ConfigureAwait ( false ) ;
200
211
201
- if ( cachedAccessTokenItem != null && ! _clientParameters . ForceRefresh )
212
+ // 2) If no cached item or force refresh is requested, return null
213
+ if ( ! IsValidCachedToken ( cachedAccessTokenItem ) )
202
214
{
203
- if ( ! string . IsNullOrEmpty ( _clientParameters . AccessTokenHashToRefresh ) )
204
- {
205
- string cachedTokenHash = ComputeSHA256 ( cachedAccessTokenItem . Secret ) ;
215
+ return null ;
216
+ }
206
217
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
+ }
214
228
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 ;
218
240
}
219
241
220
- return null ;
242
+ return true ;
221
243
}
222
244
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 )
224
249
{
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 )
230
264
{
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 ;
233
268
}
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 ( ) ;
235
286
}
236
287
237
288
private AuthenticationResult CreateAuthenticationResultFromCache ( MsalAccessTokenCacheItem cachedAccessTokenItem )
0 commit comments