Skip to content

Commit 20474b8

Browse files
Implemented Service Exception for Imds Probe (#5615)
* Implemented Service Exception for Imds Probe * Implemented GitHub Feedback * Implemented GitHub feedback * Fixed typo in unshipped files * fixed broken unit test
1 parent d36a83e commit 20474b8

File tree

15 files changed

+220
-78
lines changed

15 files changed

+220
-78
lines changed

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public static string ImdsQueryParamsHelper(
241241
return queryParams;
242242
}
243243

244-
public static async Task<bool> ProbeImdsEndpointAsync(
244+
public static async Task<(bool success, string failureReason)> ProbeImdsEndpointAsync(
245245
RequestContext requestContext,
246246
ImdsVersion imdsVersion,
247247
CancellationToken cancellationToken)
@@ -255,8 +255,8 @@ public static async Task<bool> ProbeImdsEndpointAsync(
255255
{
256256
case ImdsVersion.V2:
257257
#if NET462
258-
requestContext.Logger.Info("[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe.");
259-
return false;
258+
requestContext.Logger.Info("[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe.");
259+
return (false, "IMDSv2 is not supported on .NET Framework 4.6.2");
260260
#else
261261
apiVersionQueryParam = ImdsV2ManagedIdentitySource.ApiVersionQueryParam;
262262
imdsApiVersion = ImdsV2ManagedIdentitySource.ImdsV2ApiVersion;
@@ -303,22 +303,24 @@ public static async Task<bool> ProbeImdsEndpointAsync(
303303
retryPolicy: retryPolicy)
304304
.ConfigureAwait(false);
305305
}
306-
catch (Exception ex)
306+
catch (Exception ex) when (ex is not OperationCanceledException)
307307
{
308-
requestContext.Logger.Info($"[Managed Identity] {imdsStringHelper} probe endpoint failure. Exception occurred while sending request to probe endpoint: {ex}");
309-
return false;
308+
string failureMessage = $"{imdsStringHelper} probe failed. Exception: {ex.Message}";
309+
requestContext.Logger.Info(() => $"[Managed Identity] {failureMessage}");
310+
return (false, failureMessage);
310311
}
311312

312313
// probe omits the "Metadata: true" header and then treats 400 Bad Request as success
313314
if (response.StatusCode == HttpStatusCode.BadRequest)
314315
{
315316
requestContext.Logger.Info(() => $"[Managed Identity] {imdsStringHelper} managed identity is available.");
316-
return true;
317+
return (true, null);
317318
}
318319
else
319320
{
321+
string failureMessage = $"{imdsStringHelper} probe failed. Status code: {response.StatusCode}, Body: {response.Body}";
320322
requestContext.Logger.Info(() => $"[Managed Identity] {imdsStringHelper} managed identity is not available. Status code: {response.StatusCode}, Body: {response.Body}");
321-
return false;
323+
return (false, failureMessage);
322324
}
323325
}
324326
}

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

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ private async Task<AbstractManagedIdentity> GetOrSelectManagedIdentitySourceAsyn
5454
{
5555
requestContext.Logger.Info($"[Managed Identity] Selecting managed identity source if not cached. Cached value is {s_sourceName} ");
5656

57+
ManagedIdentitySourceResult sourceResult = null;
5758
ManagedIdentitySource source;
5859

5960
// If the source is not already set, determine it
6061
if (s_sourceName == ManagedIdentitySource.None)
6162
{
6263
// First invocation: detect and cache
63-
source = await GetManagedIdentitySourceAsync(requestContext, isMtlsPopRequested, cancellationToken).ConfigureAwait(false);
64+
sourceResult = await GetManagedIdentitySourceAsync(requestContext, isMtlsPopRequested, cancellationToken).ConfigureAwait(false);
65+
source = sourceResult.Source;
6466
}
6567
else
6668
{
@@ -96,14 +98,14 @@ private async Task<AbstractManagedIdentity> GetOrSelectManagedIdentitySourceAsyn
9698
ManagedIdentitySource.AzureArc => AzureArcManagedIdentitySource.Create(requestContext),
9799
ManagedIdentitySource.ImdsV2 => ImdsV2ManagedIdentitySource.Create(requestContext),
98100
ManagedIdentitySource.Imds => ImdsManagedIdentitySource.Create(requestContext),
99-
_ => throw new MsalClientException(MsalError.ManagedIdentityAllSourcesUnavailable, MsalErrorMessage.ManagedIdentityAllSourcesUnavailable)
101+
_ => throw CreateManagedIdentityUnavailableException(sourceResult)
100102
};
101103
}
102104
}
103105

104106
// Detect managed identity source based on the availability of environment variables and csr metadata probe request.
105107
// This method is perf sensitive any changes should be benchmarked.
106-
internal async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(
108+
internal async Task<ManagedIdentitySourceResult> GetManagedIdentitySourceAsync(
107109
RequestContext requestContext,
108110
bool isMtlsPopRequested,
109111
CancellationToken cancellationToken)
@@ -113,36 +115,46 @@ internal async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(
113115
if (source != ManagedIdentitySource.None)
114116
{
115117
s_sourceName = source;
116-
return source;
118+
return new ManagedIdentitySourceResult(source);
117119
}
118120

121+
string imdsV2FailureReason = null;
122+
string imdsV1FailureReason = null;
123+
119124
// skip the ImdsV2 probe if MtlsPop was NOT requested
120125
if (isMtlsPopRequested)
121126
{
122-
var imdsV2Response = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V2, cancellationToken).ConfigureAwait(false);
123-
if (imdsV2Response)
127+
var (imdsV2Success, imdsV2Failure) = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V2, cancellationToken).ConfigureAwait(false);
128+
if (imdsV2Success)
124129
{
125130
requestContext.Logger.Info("[Managed Identity] ImdsV2 detected.");
126131
s_sourceName = ManagedIdentitySource.ImdsV2;
127-
return s_sourceName;
132+
return new ManagedIdentitySourceResult(s_sourceName);
128133
}
134+
imdsV2FailureReason = imdsV2Failure;
129135
}
130136
else
131137
{
132138
requestContext.Logger.Info("[Managed Identity] Mtls Pop was not requested; skipping ImdsV2 probe.");
133139
}
134140

135-
var imdsV1Response = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V1, cancellationToken).ConfigureAwait(false);
136-
if (imdsV1Response)
141+
var (imdsV1Success, imdsV1Failure) = await ImdsManagedIdentitySource.ProbeImdsEndpointAsync(requestContext, ImdsVersion.V1, cancellationToken).ConfigureAwait(false);
142+
if (imdsV1Success)
137143
{
138144
requestContext.Logger.Info("[Managed Identity] ImdsV1 detected.");
139145
s_sourceName = ManagedIdentitySource.Imds;
140-
return s_sourceName;
146+
return new ManagedIdentitySourceResult(s_sourceName);
141147
}
148+
imdsV1FailureReason = imdsV1Failure;
142149

143150
requestContext.Logger.Info($"[Managed Identity] {MsalErrorMessage.ManagedIdentityAllSourcesUnavailable}");
144151
s_sourceName = ManagedIdentitySource.None;
145-
return s_sourceName;
152+
153+
return new ManagedIdentitySourceResult(s_sourceName)
154+
{
155+
ImdsV1FailureReason = imdsV1FailureReason,
156+
ImdsV2FailureReason = imdsV2FailureReason
157+
};
146158
}
147159

148160
/// <summary>
@@ -229,13 +241,40 @@ private static bool ValidateAzureArcEnvironment(string identityEndpoint, string
229241
return false;
230242
}
231243

244+
/// <summary>
245+
/// Creates an MsalClientException for when no managed identity source is available,
246+
/// including detailed failure information from IMDS probes if available.
247+
/// </summary>
248+
private static MsalClientException CreateManagedIdentityUnavailableException(ManagedIdentitySourceResult sourceResult)
249+
{
250+
string errorMessage = MsalErrorMessage.ManagedIdentityAllSourcesUnavailable;
251+
252+
if (sourceResult != null)
253+
{
254+
if (!string.IsNullOrEmpty(sourceResult.ImdsV1FailureReason) || !string.IsNullOrEmpty(sourceResult.ImdsV2FailureReason))
255+
{
256+
errorMessage += " MSAL was not able to detect the Azure Instance Metadata Service (IMDS) that runs on VMs:";
257+
if (!string.IsNullOrEmpty(sourceResult.ImdsV2FailureReason))
258+
{
259+
errorMessage += $" IMDSv2: {sourceResult.ImdsV2FailureReason}.";
260+
}
261+
if (!string.IsNullOrEmpty(sourceResult.ImdsV1FailureReason))
262+
{
263+
errorMessage += $" IMDSv1: {sourceResult.ImdsV1FailureReason}.";
264+
}
265+
}
266+
}
267+
268+
return new MsalClientException(MsalError.ManagedIdentityAllSourcesUnavailable, errorMessage);
269+
}
270+
232271
/// <summary>
233272
/// Sets (or replaces) the in-memory binding certificate used to prime the mtls_pop scheme on subsequent requests.
234273
/// The certificate is intentionally NOT disposed here to avoid invalidating caller-held references (e.g., via AuthenticationResult).
235274
/// </summary>
236275
/// <remarks>
237276
/// Lifetime considerations:
238-
/// - The binding certificate is ephemeral and valid for the tokens binding duration.
277+
/// - The binding certificate is ephemeral and valid for the token's binding duration.
239278
/// - If rotation occurs, older certificates will be eligible for GC once no longer referenced.
240279
/// - Explicit disposal can be revisited if a deterministic rotation / shutdown strategy is introduced.
241280
/// </remarks>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Identity.Client.ManagedIdentity
5+
{
6+
/// <summary>
7+
/// Result of managed identity source detection, including the detected source and any failure information from IMDS probes.
8+
/// </summary>
9+
/// <remarks>
10+
/// This class is returned by <see cref="ManagedIdentityApplication.GetManagedIdentitySourceAsync"/> to provide
11+
/// detailed information about managed identity source detection, including failure reasons when IMDS probes fail.
12+
/// This information is useful for credential chains like DefaultAzureCredential to determine whether to skip
13+
/// managed identity authentication entirely.
14+
/// </remarks>
15+
public class ManagedIdentitySourceResult
16+
{
17+
/// <summary>
18+
/// Gets the detected managed identity source.
19+
/// </summary>
20+
/// <value>
21+
/// The <see cref="ManagedIdentitySource"/> that was detected on the environment.
22+
/// Returns <see cref="ManagedIdentitySource.None"/> if no managed identity source was detected.
23+
/// </value>
24+
public ManagedIdentitySource Source { get; }
25+
26+
/// <summary>
27+
/// Gets or sets the failure reason from the IMDSv1 probe, if it failed.
28+
/// </summary>
29+
/// <value>
30+
/// A string describing why the IMDSv1 probe failed, or <c>null</c> if the probe succeeded or was not attempted.
31+
/// </value>
32+
public string ImdsV1FailureReason { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the failure reason from the IMDSv2 probe, if it failed.
36+
/// </summary>
37+
/// <value>
38+
/// A string describing why the IMDSv2 probe failed, or <c>null</c> if the probe succeeded or was not attempted.
39+
/// </value>
40+
public string ImdsV2FailureReason { get; set; }
41+
42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="ManagedIdentitySourceResult"/> class.
44+
/// </summary>
45+
/// <param name="source">The detected managed identity source.</param>
46+
public ManagedIdentitySourceResult(ManagedIdentitySource source)
47+
{
48+
Source = source;
49+
}
50+
}
51+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ public AcquireTokenForManagedIdentityParameterBuilder AcquireTokenForManagedIden
5656
}
5757

5858
/// <inheritdoc/>
59-
public async Task<ManagedIdentitySource> GetManagedIdentitySourceAsync(CancellationToken cancellationToken)
59+
public async Task<ManagedIdentitySourceResult> GetManagedIdentitySourceAsync(CancellationToken cancellationToken)
6060
{
6161
if (ManagedIdentityClient.s_sourceName != ManagedIdentitySource.None)
6262
{
63-
return ManagedIdentityClient.s_sourceName;
63+
return new ManagedIdentitySourceResult(ManagedIdentityClient.s_sourceName);
6464
}
6565

6666
// Create a temporary RequestContext for the logger and the IMDS probe request.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,5 +1232,10 @@ public static class MsalError
12321232
/// All managed identity sources are unavailable.
12331233
/// </summary>
12341234
public const string ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable";
1235+
1236+
/// <summary>
1237+
/// Represents the error code returned when an IMDS operation fails.
1238+
/// </summary>
1239+
public const string ImdsServiceError = "imds_service_error";
12351240
}
12361241
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
1+
const Microsoft.Identity.Client.MsalError.ImdsServiceError = "imds_service_error" -> string
2+
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
23
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2
34
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
45
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task<bool>
56
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
67
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
78
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary<string, string>
8-
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
9+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult
10+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string
11+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void
12+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string
13+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void
14+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ManagedIdentitySourceResult(Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource source) -> void
15+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.Source.get -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
16+
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult>
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
1+
const Microsoft.Identity.Client.MsalError.ImdsServiceError = "imds_service_error" -> string
2+
const Microsoft.Identity.Client.MsalError.ManagedIdentityAllSourcesUnavailable = "managed_identity_all_sources_unavailable" -> string
23
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2
34
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.FormatResultAsync(Microsoft.Identity.Client.AuthenticationResult authenticationResult, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
45
Microsoft.Identity.Client.AuthScheme.IAuthenticationOperation2.ValidateCachedTokenAsync(Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData cachedTokenData) -> System.Threading.Tasks.Task<bool>
56
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData
67
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.MsalCacheValidationData() -> void
78
Microsoft.Identity.Client.AuthScheme.MsalCacheValidationData.PersistedCacheParameters.get -> System.Collections.Generic.IDictionary<string, string>
8-
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource>
9+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult
10+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.get -> string
11+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV1FailureReason.set -> void
12+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.get -> string
13+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ImdsV2FailureReason.set -> void
14+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.ManagedIdentitySourceResult(Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource source) -> void
15+
Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult.Source.get -> Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySource
16+
Microsoft.Identity.Client.ManagedIdentityApplication.GetManagedIdentitySourceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Identity.Client.ManagedIdentity.ManagedIdentitySourceResult>

0 commit comments

Comments
 (0)