Skip to content

Commit 625a9d3

Browse files
new-feature
1 parent 622fa2a commit 625a9d3

File tree

13 files changed

+317
-3
lines changed

13 files changed

+317
-3
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ 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+
161181
/// <inheritdoc/>
162182
internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationToken cancellationToken)
163183
{
@@ -198,6 +218,14 @@ protected override void Validate()
198218

199219
base.Validate();
200220

221+
// Force refresh + AccessTokenHashToRefresh APIs cannot be used together
222+
if (Parameters.ForceRefresh && !string.IsNullOrEmpty(Parameters.AccessTokenHashToRefresh))
223+
{
224+
throw new MsalClientException(
225+
MsalError.ForceRefreshNotCompatibleWithTokenHash,
226+
MsalErrorMessage.ForceRefreshAndTokenHasNotCompatible);
227+
}
228+
201229
if (Parameters.SendX5C == null)
202230
{
203231
Parameters.SendX5C = this.ServiceBundle.Config.SendX5C;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ namespace Microsoft.Identity.Client.ApiConfig.Parameters
99
{
1010
internal class AcquireTokenForClientParameters : AbstractAcquireTokenConfidentialClientParameters, IAcquireTokenParameters
1111
{
12+
public bool ForceRefresh { get; set; }
13+
1214
/// <summary>
15+
/// The SHA-256 hash of the access token that should be refreshed.
16+
/// If set, token refresh will occur only if a matching token is found in cache.
1317
/// </summary>
14-
public bool ForceRefresh { get; set; }
18+
public string AccessTokenHashToRefresh { get; set; }
1519

1620
/// <inheritdoc/>
1721
public void LogParameters(ILoggerAdapter logger)

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

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

4+
using System;
45
using System.Collections.Generic;
6+
using System.Security.Cryptography;
7+
using System.Text;
58
using System.Threading;
69
using System.Threading.Tasks;
710
using Microsoft.Identity.Client.ApiConfig.Parameters;
@@ -47,8 +50,12 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
4750

4851
AuthenticationResult authResult;
4952

53+
bool skipCache = _clientParameters.ForceRefresh ||
54+
(!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims) &&
55+
string.IsNullOrEmpty(_clientParameters.AccessTokenHashToRefresh));
56+
5057
// Skip checking cache when force refresh or claims are specified
51-
if (_clientParameters.ForceRefresh || !string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
58+
if (skipCache)
5259
{
5360
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims;
5461
logger.Info("[ClientCredentialRequest] Skipped looking for a cached access token because ForceRefresh or Claims were set.");
@@ -193,6 +200,18 @@ private async Task<MsalAccessTokenCacheItem> GetCachedAccessTokenAsync()
193200

194201
if (cachedAccessTokenItem != null && !_clientParameters.ForceRefresh)
195202
{
203+
if (!string.IsNullOrEmpty(_clientParameters.AccessTokenHashToRefresh))
204+
{
205+
string cachedTokenHash = ComputeSHA256(cachedAccessTokenItem.Secret);
206+
207+
if (string.Equals(cachedTokenHash, _clientParameters.AccessTokenHashToRefresh, StringComparison.Ordinal))
208+
{
209+
AuthenticationRequestParameters.RequestContext.Logger.Info(
210+
"[ClientCredentialRequest] Found the 'bad' token in the cache. Skipping cache usage.");
211+
return null; // triggers a new token acquisition
212+
}
213+
}
214+
196215
AuthenticationRequestParameters.RequestContext.ApiEvent.IsAccessTokenCacheHit = true;
197216
Metrics.IncrementTotalAccessTokensFromCache();
198217
return cachedAccessTokenItem;
@@ -201,6 +220,20 @@ private async Task<MsalAccessTokenCacheItem> GetCachedAccessTokenAsync()
201220
return null;
202221
}
203222

223+
private static string ComputeSHA256(string token)
224+
{
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())
230+
{
231+
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
232+
return Convert.ToBase64String(hashBytes);
233+
}
234+
#endif
235+
}
236+
204237
private AuthenticationResult CreateAuthenticationResultFromCache(MsalAccessTokenCacheItem cachedAccessTokenItem)
205238
{
206239
AuthenticationResult authResult = new AuthenticationResult(

src/client/Microsoft.Identity.Client/MsalError.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,5 +1177,16 @@ public static class MsalError
11771177
/// <para>Mitigation:</para> Ensure that the AzureRegion configuration is set when using mTLS PoP as it requires a regional endpoint.
11781178
/// </summary>
11791179
public const string RegionRequiredForMtlsPop = "region_required_for_mtls_pop";
1180+
1181+
/// <summary>
1182+
/// <para>What happened?</para> The operation attempted to force a token refresh while also using a token hash.
1183+
/// These two options are incompatible because forcing a refresh bypasses token caching,
1184+
/// which conflicts with token hash validation.
1185+
/// <para>Mitigation:</para>
1186+
/// - Ensure that `force_refresh` is not set to `true` when using a token hash.
1187+
/// - Review the request parameters to ensure they are not conflicting.
1188+
/// - If token hashing is required, allow the cached token to be used instead of forcing a refresh.
1189+
/// </summary>
1190+
public const string ForceRefreshNotCompatibleWithTokenHash = "force_refresh_and_token_hash_not_compatible";
11801191
}
11811192
}

src/client/Microsoft.Identity.Client/MsalErrorMessage.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,5 +437,6 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
437437
public const string MtlsInvalidAuthorityTypeMessage = "mTLS PoP is only supported for AAD authority type. See https://aka.ms/msal-net-pop for details.";
438438
public const string MtlsNonTenantedAuthorityNotAllowedMessage = "mTLS authentication requires a tenanted authority. Using 'common', 'organizations', or similar non-tenanted authorities is not allowed. Please provide an authority with a specific tenant ID (e.g., 'https://login.microsoftonline.com/{tenantId}'). See https://aka.ms/msal-net-pop for details.";
439439
public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details.";
440+
public const string ForceRefreshAndTokenHasNotCompatible = "Cannot specify ForceRefresh and AccessTokenSha256ToRefresh in the same request.";
440441
}
441442
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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

0 commit comments

Comments
 (0)