diff --git a/CHANGELOG.md b/CHANGELOG.md index c98b4b248e..5934f69ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### New Features - Exposed canonical OpenTelemetry tag names per metric via `MsalMetricsCatalog.CanonicalTagsByMetric` for discoverability and validation. [#6076](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/6076) +### Changes +- Managed identity error messages and request-failure logs now include the detected `ManagedIdentitySource` (e.g., `AppService`, `Imds`, `ServiceFabric`) so the host-issued `Managed Identity Correlation ID` can be traced to the correct host's telemetry. [#6101](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/6101) + 4.85.0 ====== diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs index a4771ba7fc..fd91737bbd 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -188,7 +188,7 @@ protected virtual Task HandleResponseAsync( string message = GetMessageFromErrorResponse(response); - _requestContext.Logger.Error($"[Managed Identity] request failed, HttpStatusCode: {response.StatusCode} Error message: {message}"); + _requestContext.Logger.Error($"[Managed Identity] request failed, Source: {_sourceType}, HttpStatusCode: {response.StatusCode} Error message: {message}"); MsalException exception = MsalServiceExceptionFactory.CreateManagedIdentityException( MsalError.ManagedIdentityRequestFailed, @@ -281,7 +281,10 @@ private string ExtractErrorMessageFromManagedIdentityErrorResponse(ManagedIdenti if (!string.IsNullOrEmpty(managedIdentityErrorResponse.CorrelationId)) { - stringBuilder.Append($"Managed Identity Correlation ID: {managedIdentityErrorResponse.CorrelationId} Use this Correlation ID for further investigation."); + stringBuilder.Append( + $"Managed Identity Correlation ID: {managedIdentityErrorResponse.CorrelationId} " + + $"(issued by the '{_sourceType}' managed identity source; search that source's telemetry with this correlation ID). " + + $"Use this Correlation ID for further investigation."); } if (stringBuilder.Length == ManagedIdentityPrefix.Length) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 849039f0e4..4b20bfee3e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -559,8 +559,8 @@ await mi.AcquireTokenForManagedIdentity(resource) } [TestMethod] - [DataRow("{\"statusCode\":500,\"message\":\"Error message\",\"correlationId\":\"GUID\"}", new string[] { "Error message", "GUID" })] - [DataRow("{\"message\":\"Error message\",\"correlationId\":\"GUID\"}", new string[] { "Error message", "GUID" })] + [DataRow("{\"statusCode\":500,\"message\":\"Error message\",\"correlationId\":\"GUID\"}", new string[] { "Error message", "GUID", "issued by the 'AppService' managed identity source" })] + [DataRow("{\"message\":\"Error message\",\"correlationId\":\"GUID\"}", new string[] { "Error message", "GUID", "issued by the 'AppService' managed identity source" })] [DataRow("{\"error\":\"errorCode\",\"error_description\":\"Error message\"}", new string[] { "errorCode", "Error message" })] [DataRow("{\"error_description\":\"Error message\"}", new string[] { "Error message" })] [DataRow("{\"message\":\"Error message\"}", new string[] { "Error message" })] @@ -606,6 +606,48 @@ await mi.AcquireTokenForManagedIdentity(Resource) } } + // The correlation-ID branch that attributes the host-issued correlation ID to a managed + // identity source is source-agnostic, so this guards it against regression for a + // non-AppService source (Imds) as well. It also asserts the full composed phrase rather + // than a bare source name, so the check cannot pass merely because the source name appears + // in the echoed error body. + [TestMethod] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.Imds, ImdsEndpoint)] + public async Task ManagedIdentityErrorMessageAttributesCorrelationIdToSourceAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + var mi = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager) + .Build(); + + // camelCase "correlationId" maps to ManagedIdentityErrorResponse.CorrelationId, which + // triggers the correlation-ID branch that names the managed identity source. + const string errorResponse = "{\"statusCode\":500,\"message\":\"Error message\",\"correlationId\":\"some-correlation-id\"}"; + + httpManager.AddManagedIdentityMockHandler(endpoint, Resource, errorResponse, + managedIdentitySource, statusCode: HttpStatusCode.InternalServerError); + + MsalServiceException ex = await Assert.ThrowsAsync(async () => + await mi.AcquireTokenForManagedIdentity(Resource) + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + + Assert.IsNotNull(ex); + Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); + Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); + + // Assert the exact composed phrase (position-independent-proof): only MSAL's message + // builder produces this text, so it cannot be satisfied by the echoed error body. + string expectedSourcePhrase = $"issued by the '{managedIdentitySource}' managed identity source"; + Assert.Contains(expectedSourcePhrase, ex.Message, + $"Expected the message to attribute the correlation ID to the '{managedIdentitySource}' source. Actual error message: {ex.Message}"); + } + } + [TestMethod] [DataRow("", ManagedIdentitySource.AppService, AppServiceEndpoint)] [DataRow(null, ManagedIdentitySource.AppService, AppServiceEndpoint)]