1
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
2
// Licensed under the MIT License.
3
3
4
+ using System ;
4
5
using System . Collections . Generic ;
6
+ using System . Security . Cryptography ;
7
+ using System . Text ;
8
+ using System . Text . RegularExpressions ;
5
9
using System . Threading ;
6
10
using System . Threading . Tasks ;
7
11
using Microsoft . Identity . Client . ApiConfig . Parameters ;
10
14
using Microsoft . Identity . Client . Extensibility ;
11
15
using Microsoft . Identity . Client . Instance ;
12
16
using Microsoft . Identity . Client . OAuth2 ;
17
+ using Microsoft . Identity . Client . PlatformsCommon . Interfaces ;
13
18
using Microsoft . Identity . Client . Utils ;
14
19
15
20
namespace Microsoft . Identity . Client . Internal . Requests
@@ -18,6 +23,7 @@ internal class ClientCredentialRequest : RequestBase
18
23
{
19
24
private readonly AcquireTokenForClientParameters _clientParameters ;
20
25
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim ( 1 , 1 ) ;
26
+ private readonly ICryptographyManager _cryptoManager ;
21
27
22
28
public ClientCredentialRequest (
23
29
IServiceBundle serviceBundle ,
@@ -26,6 +32,7 @@ public ClientCredentialRequest(
26
32
: base ( serviceBundle , authenticationRequestParameters , clientParameters )
27
33
{
28
34
_clientParameters = clientParameters ;
35
+ _cryptoManager = serviceBundle . PlatformProxy . CryptographyManager ;
29
36
}
30
37
31
38
protected override async Task < AuthenticationResult > ExecuteAsync ( CancellationToken cancellationToken )
@@ -44,14 +51,22 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
44
51
{
45
52
logger . Error ( MsalErrorMessage . ClientCredentialWrongAuthority ) ;
46
53
}
47
-
48
54
AuthenticationResult authResult ;
49
55
50
- // Skip checking cache when force refresh or claims are specified
51
- if ( _clientParameters . ForceRefresh || ! string . IsNullOrEmpty ( AuthenticationRequestParameters . Claims ) )
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.
62
+ bool skipCache = _clientParameters . ForceRefresh ||
63
+ ( ! string . IsNullOrEmpty ( AuthenticationRequestParameters . Claims ) &&
64
+ string . IsNullOrEmpty ( _clientParameters . AccessTokenHashToRefresh ) ) ;
65
+
66
+ if ( skipCache )
52
67
{
53
68
AuthenticationRequestParameters . RequestContext . ApiEvent . CacheInfo = CacheRefreshReason . ForceRefreshOrClaims ;
54
- logger . Info ( "[ClientCredentialRequest] Skipped looking for a cached access token because ForceRefresh or Claims were set." ) ;
69
+ logger . Info ( "[ClientCredentialRequest] Skipped looking for a cached access token because either of ForceRefresh, Claims or AccessTokenHashToRefresh were set." ) ;
55
70
authResult = await GetAccessTokenAsync ( cancellationToken , logger ) . ConfigureAwait ( false ) ;
56
71
return authResult ;
57
72
}
@@ -187,20 +202,78 @@ private async Task<AuthenticationResult> SendTokenRequestToAppTokenProviderAsync
187
202
return authResult ;
188
203
}
189
204
205
+ /// <summary>
206
+ /// Checks if the token should be used from the cache and returns the cached access token if applicable.
207
+ /// </summary>
208
+ /// <returns></returns>
190
209
private async Task < MsalAccessTokenCacheItem > GetCachedAccessTokenAsync ( )
191
210
{
192
- MsalAccessTokenCacheItem cachedAccessTokenItem = await CacheManager . FindAccessTokenAsync ( ) . ConfigureAwait ( false ) ;
211
+ // Fetch the cache item (could be null if none found).
212
+ MsalAccessTokenCacheItem cacheItem =
213
+ await CacheManager . FindAccessTokenAsync ( ) . ConfigureAwait ( false ) ;
193
214
194
- if ( cachedAccessTokenItem != null && ! _clientParameters . ForceRefresh )
215
+ // If the item fails any checks (null, or hash mismatch),
216
+ if ( ! ShouldUseCachedToken ( cacheItem ) )
195
217
{
196
- AuthenticationRequestParameters . RequestContext . ApiEvent . IsAccessTokenCacheHit = true ;
197
- Metrics . IncrementTotalAccessTokensFromCache ( ) ;
198
- return cachedAccessTokenItem ;
218
+ return null ;
199
219
}
200
220
201
- return null ;
221
+ // Otherwise, record a successful cache hit and return the token.
222
+ MarkAccessTokenAsCacheHit ( ) ;
223
+ return cacheItem ;
224
+ }
225
+
226
+ /// <summary>
227
+ /// Checks if the token should be used from the cache.
228
+ /// </summary>
229
+ /// <param name="cacheItem"></param>
230
+ /// <returns></returns>
231
+ private bool ShouldUseCachedToken ( MsalAccessTokenCacheItem cacheItem )
232
+ {
233
+ // 1) No cached item
234
+ if ( cacheItem == null )
235
+ {
236
+ return false ;
237
+ }
238
+
239
+ // 2) If the token’s hash matches AccessTokenHashToRefresh, ignore it
240
+ if ( ! string . IsNullOrEmpty ( _clientParameters . AccessTokenHashToRefresh ) &&
241
+ IsMatchingTokenHash ( cacheItem . Secret , _clientParameters . AccessTokenHashToRefresh ) )
242
+ {
243
+ AuthenticationRequestParameters . RequestContext . Logger . Info (
244
+ "[ClientCredentialRequest] A cached token was found and its hash matches AccessTokenHashToRefresh, so it is ignored." ) ;
245
+ return false ;
246
+ }
247
+
248
+ return true ;
249
+ }
250
+
251
+ /// <summary>
252
+ /// Checks if the token hash matches the hash provided in AccessTokenHashToRefresh.
253
+ /// </summary>
254
+ /// <param name="tokenSecret"></param>
255
+ /// <param name="accessTokenHashToRefresh"></param>
256
+ /// <returns></returns>
257
+ private bool IsMatchingTokenHash ( string tokenSecret , string accessTokenHashToRefresh )
258
+ {
259
+ string cachedTokenHash = _cryptoManager . CreateSha256Hash ( tokenSecret ) ;
260
+ return string . Equals ( cachedTokenHash , accessTokenHashToRefresh , StringComparison . Ordinal ) ;
261
+ }
262
+
263
+ /// <summary>
264
+ /// Marks the request as a cache hit and increments the cache hit count.
265
+ /// </summary>
266
+ private void MarkAccessTokenAsCacheHit ( )
267
+ {
268
+ AuthenticationRequestParameters . RequestContext . ApiEvent . IsAccessTokenCacheHit = true ;
269
+ Metrics . IncrementTotalAccessTokensFromCache ( ) ;
202
270
}
203
271
272
+ /// <summary>
273
+ /// returns the cached access token item
274
+ /// </summary>
275
+ /// <param name="cachedAccessTokenItem"></param>
276
+ /// <returns></returns>
204
277
private AuthenticationResult CreateAuthenticationResultFromCache ( MsalAccessTokenCacheItem cachedAccessTokenItem )
205
278
{
206
279
AuthenticationResult authResult = new AuthenticationResult (
@@ -216,6 +289,11 @@ private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessToken
216
289
return authResult ;
217
290
}
218
291
292
+ /// <summary>
293
+ /// Gets overriden scopes for client credentials flow
294
+ /// </summary>
295
+ /// <param name="inputScopes"></param>
296
+ /// <returns></returns>
219
297
protected override SortedSet < string > GetOverriddenScopes ( ISet < string > inputScopes )
220
298
{
221
299
// Client credentials should not add the reserved scopes
@@ -224,6 +302,10 @@ protected override SortedSet<string> GetOverriddenScopes(ISet<string> inputScope
224
302
return new SortedSet < string > ( inputScopes ) ;
225
303
}
226
304
305
+ /// <summary>
306
+ /// Gets the body parameters for the client credentials flow
307
+ /// </summary>
308
+ /// <returns></returns>
227
309
private Dictionary < string , string > GetBodyParameters ( )
228
310
{
229
311
var dict = new Dictionary < string , string >
@@ -235,6 +317,11 @@ private Dictionary<string, string> GetBodyParameters()
235
317
return dict ;
236
318
}
237
319
320
+ /// <summary>
321
+ /// Gets the CCS header for the client credentials flow
322
+ /// </summary>
323
+ /// <param name="additionalBodyParameters"></param>
324
+ /// <returns></returns>
238
325
protected override KeyValuePair < string , string > ? GetCcsHeader ( IDictionary < string , string > additionalBodyParameters )
239
326
{
240
327
return null ;
0 commit comments