Skip to content

Commit 10ff824

Browse files
Add Service Fabric token revocation support (#5421)
* main * pr comments * address pr comments * pr comments --------- Co-authored-by: Gladwin Johnson <[email protected]>
1 parent 6cb983d commit 10ff824

File tree

15 files changed

+391
-48
lines changed

15 files changed

+391
-48
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter
1616

1717
public string Resource { get; set; }
1818

19+
public string Claims { get; set; }
20+
21+
public string RevokedTokenHash { get; set; }
22+
1923
public void LogParameters(ILoggerAdapter logger)
2024
{
2125
if (logger.IsLoggingEnabled(LogLevel.Info))
@@ -25,6 +29,8 @@ public void LogParameters(ILoggerAdapter logger)
2529
=== AcquireTokenForManagedIdentityParameters ===
2630
ForceRefresh: {ForceRefresh}
2731
Resource: {Resource}
32+
Claims: {!string.IsNullOrEmpty(Claims)}
33+
RevokedTokenHash: {!string.IsNullOrEmpty(RevokedTokenHash)}
2834
""");
2935
}
3036
}

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

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.Identity.Client.Core;
1111
using Microsoft.Identity.Client.ManagedIdentity;
1212
using Microsoft.Identity.Client.OAuth2;
13+
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
1314
using Microsoft.Identity.Client.Utils;
1415

1516
namespace Microsoft.Identity.Client.Internal.Requests
@@ -18,6 +19,7 @@ internal class ManagedIdentityAuthRequest : RequestBase
1819
{
1920
private readonly AcquireTokenForManagedIdentityParameters _managedIdentityParameters;
2021
private static readonly SemaphoreSlim s_semaphoreSlim = new SemaphoreSlim(1, 1);
22+
private readonly ICryptographyManager _cryptoManager;
2123

2224
public ManagedIdentityAuthRequest(
2325
IServiceBundle serviceBundle,
@@ -26,28 +28,62 @@ public ManagedIdentityAuthRequest(
2628
: base(serviceBundle, authenticationRequestParameters, managedIdentityParameters)
2729
{
2830
_managedIdentityParameters = managedIdentityParameters;
31+
_cryptoManager = serviceBundle.PlatformProxy.CryptographyManager;
2932
}
3033

3134
protected override async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellationToken)
3235
{
3336
AuthenticationResult authResult = null;
3437
ILoggerAdapter logger = AuthenticationRequestParameters.RequestContext.Logger;
3538

36-
// Skip checking cache when force refresh or claims is specified
37-
if (_managedIdentityParameters.ForceRefresh || !string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
39+
// 1. FIRST, handle ForceRefresh
40+
if (_managedIdentityParameters.ForceRefresh)
3841
{
42+
//log a warning if Claims are also set
43+
if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
44+
{
45+
logger.Warning("[ManagedIdentityRequest] Both ForceRefresh and Claims are set. Using ForceRefresh to skip cache.");
46+
}
47+
3948
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims;
40-
41-
logger.Info("[ManagedIdentityRequest] Skipped looking for a cached access token because ForceRefresh or Claims were set. " +
42-
"This means either a force refresh was requested or claims were present.");
49+
logger.Info("[ManagedIdentityRequest] Skipped using the cache because ForceRefresh was set.");
4350

51+
// Straight to the MI endpoint
4452
authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false);
4553
return authResult;
4654
}
4755

48-
MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync().ConfigureAwait(false);
56+
// 2. Otherwise, look for a cached token
57+
MsalAccessTokenCacheItem cachedAccessTokenItem = await GetCachedAccessTokenAsync()
58+
.ConfigureAwait(false);
59+
60+
// If we have claims, we do NOT use the cached token (but we still need it to compute the hash).
61+
if (!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
62+
{
63+
_managedIdentityParameters.Claims = AuthenticationRequestParameters.Claims;
64+
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.ForceRefreshOrClaims;
4965

50-
// No access token or cached access token needs to be refreshed
66+
// If there is a cached token, compute its hash for the “revoked token” scenario
67+
if (cachedAccessTokenItem != null)
68+
{
69+
string cachedTokenHash = _cryptoManager.CreateSha256HashHex(cachedAccessTokenItem.Secret);
70+
_managedIdentityParameters.RevokedTokenHash = cachedTokenHash;
71+
72+
logger.Info("[ManagedIdentityRequest] Claims are present. Computed hash of the cached (revoked) token. " +
73+
"Will now request a fresh token from the MI endpoint.");
74+
}
75+
else
76+
{
77+
logger.Info("[ManagedIdentityRequest] Claims are present, but no cached token was found. " +
78+
"Requesting a fresh token from the MI endpoint without a revoked-token hash.");
79+
}
80+
81+
// In both cases, we skip using the cached token and get a new one
82+
authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false);
83+
return authResult;
84+
}
85+
86+
// 3. If we have no ForceRefresh and no claims, we can use the cache
5187
if (cachedAccessTokenItem != null)
5288
{
5389
authResult = CreateAuthenticationResultFromCache(cachedAccessTokenItem);
@@ -67,30 +103,33 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
67103

68104
SilentRequestHelper.ProcessFetchInBackground(
69105
cachedAccessTokenItem,
70-
() =>
71-
{
72-
// Use a linked token source, in case the original cancellation token source is disposed before this background task completes.
73-
using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
74-
return GetAccessTokenAsync(tokenSource.Token, logger);
75-
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
106+
() =>
107+
{
108+
// Use a linked token source, in case the original cancellation token source is disposed before this background task completes.
109+
using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
110+
return GetAccessTokenAsync(tokenSource.Token, logger);
111+
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
76112
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
77113
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);
78114
}
79115
}
80116
catch (MsalServiceException e)
81117
{
118+
// If background refresh fails, we handle the exception
82119
return await HandleTokenRefreshErrorAsync(e, cachedAccessTokenItem).ConfigureAwait(false);
83120
}
84121
}
85122
else
86123
{
87-
// No AT in the cache
124+
// No cached token
88125
if (AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo != CacheRefreshReason.Expired)
89126
{
90127
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo = CacheRefreshReason.NoCachedAccessToken;
91128
}
92129

93-
logger.Info("[ManagedIdentityRequest] No cached access token. Getting a token from the managed identity endpoint.");
130+
logger.Info("[ManagedIdentityRequest] No cached access token found. " +
131+
"Getting a token from the managed identity endpoint.");
132+
94133
authResult = await GetAccessTokenAsync(cancellationToken, logger).ConfigureAwait(false);
95134
}
96135

@@ -112,12 +151,15 @@ private async Task<AuthenticationResult> GetAccessTokenAsync(
112151

113152
try
114153
{
115-
// Bypass cache and send request to token endpoint, when
116-
// 1. Force refresh is requested, or
117-
// 2. If the access token needs to be refreshed proactively.
154+
// While holding the semaphore, decide whether to bypass the cache.
155+
// Re-check because another thread may have filled the cache while we waited.
156+
// Bypass when:
157+
// 1) ForceRefresh is requested
158+
// 2) Proactive refresh is in effect
159+
// 3) Claims are present (revocation flow)
118160
if (_managedIdentityParameters.ForceRefresh ||
119161
AuthenticationRequestParameters.RequestContext.ApiEvent.CacheInfo == CacheRefreshReason.ProactivelyRefreshed ||
120-
!string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
162+
!string.IsNullOrEmpty(_managedIdentityParameters.Claims))
121163
{
122164
authResult = await SendTokenRequestForManagedIdentityAsync(logger, cancellationToken).ConfigureAwait(false);
123165
}

src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
using System.Security.Cryptography.X509Certificates;
1616
using System.Net.Security;
1717
using Microsoft.Identity.Client.Http.Retry;
18-
19-
18+
using System.Collections.Generic;
19+
using System.Linq;
2020
#if SUPPORTS_SYSTEM_TEXT_JSON
2121
using System.Text.Json;
2222
#else
@@ -57,6 +57,15 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
5757

5858
ManagedIdentityRequest request = CreateRequest(resource);
5959

60+
// Automatically add claims / capabilities if this MI source supports them
61+
if (_sourceType.SupportsClaimsAndCapabilities())
62+
{
63+
request.AddClaimsAndCapabilities(
64+
_requestContext.ServiceBundle.Config.ClientCapabilities,
65+
parameters,
66+
_requestContext.Logger);
67+
}
68+
6069
_requestContext.Logger.Info("[Managed Identity] Sending request to managed identity endpoints.");
6170

6271
IRetryPolicy retryPolicy = _requestContext.ServiceBundle.Config.RetryPolicyFactory.GetRetryPolicy(request.RequestType);

src/client/Microsoft.Identity.Client/ManagedIdentity/AppServiceManagedIdentitySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Globalization;
7+
using Microsoft.Identity.Client.ApiConfig.Parameters;
78
using Microsoft.Identity.Client.Core;
89
using Microsoft.Identity.Client.Internal;
910
using Microsoft.Identity.Client.Utils;

src/client/Microsoft.Identity.Client/ManagedIdentity/CloudShellManagedIdentitySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Globalization;
66
using System.Net.Http;
7+
using Microsoft.Identity.Client.ApiConfig.Parameters;
78
using Microsoft.Identity.Client.Core;
89
using Microsoft.Identity.Client.Internal;
910

src/client/Microsoft.Identity.Client/ManagedIdentity/MachineLearningManagedIdentitySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Globalization;
6+
using Microsoft.Identity.Client.ApiConfig.Parameters;
67
using Microsoft.Identity.Client.Core;
78
using Microsoft.Identity.Client.Internal;
89

src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net.Http;
8+
using Microsoft.Identity.Client.ApiConfig.Parameters;
9+
using Microsoft.Identity.Client.Core;
10+
using Microsoft.Identity.Client.OAuth2;
711
using Microsoft.Identity.Client.Utils;
812

913
namespace Microsoft.Identity.Client.ManagedIdentity
@@ -39,5 +43,26 @@ public Uri ComputeUri()
3943

4044
return uriBuilder.Uri;
4145
}
46+
47+
internal void AddClaimsAndCapabilities(
48+
IEnumerable<string> clientCapabilities,
49+
AcquireTokenForManagedIdentityParameters parameters,
50+
ILoggerAdapter logger)
51+
{
52+
// xms_cc – client capabilities
53+
if (clientCapabilities != null && clientCapabilities.Any())
54+
{
55+
QueryParameters["xms_cc"] = string.Join(",", clientCapabilities);
56+
logger.Info("[Managed Identity] Adding client capabilities (xms_cc) to Managed Identity request.");
57+
}
58+
59+
// token_sha256_to_refresh – only when both claims and hash are present
60+
if (!string.IsNullOrEmpty(parameters.Claims) &&
61+
!string.IsNullOrEmpty(parameters.RevokedTokenHash))
62+
{
63+
QueryParameters["token_sha256_to_refresh"] = parameters.RevokedTokenHash;
64+
logger.Info("[Managed Identity] Passing SHA-256 of the 'revoked' token to Managed Identity endpoint.");
65+
}
66+
}
4267
}
4368
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Identity.Client.ManagedIdentity
7+
{
8+
internal static class ManagedIdentitySourceExtensions
9+
{
10+
private static readonly HashSet<ManagedIdentitySource> s_supportsClaimsAndCaps =
11+
[
12+
// add other sources here as they light up
13+
ManagedIdentitySource.ServiceFabric,
14+
];
15+
16+
internal static bool SupportsClaimsAndCapabilities(
17+
this ManagedIdentitySource source) => s_supportsClaimsAndCaps.Contains(source);
18+
}
19+
}

src/client/Microsoft.Identity.Client/ManagedIdentity/ServiceFabricManagedIdentitySource.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Http;
77
using System.Net.Security;
88
using System.Security.Cryptography.X509Certificates;
9+
using Microsoft.Identity.Client.ApiConfig.Parameters;
910
using Microsoft.Identity.Client.Core;
1011
using Microsoft.Identity.Client.Internal;
1112

@@ -34,9 +35,9 @@ public static AbstractManagedIdentity Create(RequestContext requestContext)
3435
var exception = MsalServiceExceptionFactory.CreateManagedIdentityException(
3536
MsalError.InvalidManagedIdentityEndpoint,
3637
errorMessage,
37-
null,
38+
null,
3839
ManagedIdentitySource.ServiceFabric,
39-
null);
40+
null);
4041

4142
throw exception;
4243
}

src/client/Microsoft.Identity.Client/Utils/CoreHelpers.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,22 +75,33 @@ public static string ToQueryParameter(this IDictionary<string, string> input)
7575
return builder.ToString();
7676
}
7777

78-
public static Dictionary<string, string> ParseKeyValueList(string input, char delimiter, bool urlDecode,
78+
public static Dictionary<string, string> ParseKeyValueList(
79+
string input,
80+
char delimiter,
81+
bool urlDecode,
7982
bool lowercaseKeys,
8083
RequestContext requestContext)
8184
{
8285
var response = new Dictionary<string, string>();
8386

87+
// Split the full query string on & (or any provided delimiter) to get individual k=v pairs.
8488
var queryPairs = SplitWithQuotes(input, delimiter);
8589

8690
foreach (string queryPair in queryPairs)
8791
{
88-
var pair = SplitWithQuotes(queryPair, '=');
92+
// Instead of splitting on *all* '=' characters, find only the first one.
93+
// This ensures that if the value itself contains '=', such as a trailing '=' in Base64,
94+
// we do not accidentally split the base64 value into extra parts and lose the padding.
95+
int idx = queryPair.IndexOf('=');
8996

90-
if (pair.Count == 2 && !string.IsNullOrWhiteSpace(pair[0]) && !string.IsNullOrWhiteSpace(pair[1]))
97+
// idx > 0 means we found an '=' and have a valid key substring before it
98+
if (idx > 0)
9199
{
92-
string key = pair[0];
93-
string value = pair[1];
100+
// The key is everything before the first '='
101+
string key = queryPair.Substring(0, idx);
102+
103+
// The value is everything after the first '=' (including any trailing '=')
104+
string value = queryPair.Substring(idx + 1);
94105

95106
// Url decoding is needed for parsing OAuth response, but not for parsing WWW-Authenticate header in 401 challenge
96107
if (urlDecode)
@@ -99,16 +110,19 @@ public static Dictionary<string, string> ParseKeyValueList(string input, char de
99110
value = UrlDecode(value);
100111
}
101112

113+
// Optionally convert key to lowercase
102114
if (lowercaseKeys)
103115
{
104116
key = key.Trim().ToLowerInvariant();
105117
}
106118

119+
// Trim quotes and whitespace around the value
107120
value = value.Trim().Trim('\"').Trim();
108121

109122
if (response.ContainsKey(key))
110123
{
111-
requestContext?.Logger.Warning(string.Format(CultureInfo.InvariantCulture,
124+
requestContext?.Logger.Warning(
125+
string.Format(CultureInfo.InvariantCulture,
112126
"Key/value pair list contains redundant key '{0}'.", key));
113127
}
114128

0 commit comments

Comments
 (0)