@@ -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 token’ s 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>
0 commit comments