diff --git a/path/to/ClaimsTest.cs b/path/to/ClaimsTest.cs new file mode 100644 index 0000000000..471912ed67 --- /dev/null +++ b/path/to/ClaimsTest.cs @@ -0,0 +1,10 @@ + [TestMethod] + public void ClaimsHelper_ErrorMessage_HandlesEmptyEnvVariable() + { + // Test that the error message handles empty environment variable correctly + string errorMsg = MsalErrorMessage.JsonEncoderIntrinsicsUnsupported("Arm64", true, ""); + Assert.IsTrue(errorMsg.Contains("JSON encoding failed")); + Assert.IsTrue(errorMsg.Contains("Arm64")); + Assert.IsTrue(errorMsg.Contains("Is 64-bit process: True")); + Assert.IsTrue(errorMsg.Contains("DOTNET_EnableHWIntrinsic: (not set)")); + } diff --git a/src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs b/src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs index b3770ac8c0..6952b6510f 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClaimsHelper.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Microsoft.Identity.Client.Utils; #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text; @@ -29,10 +31,21 @@ internal static string GetMergedClaimsAndClientCapabilities( { if (clientCapabilities != null && clientCapabilities.Any()) { - JObject capabilitiesJson = CreateClientCapabilitiesRequestJson(clientCapabilities); - JObject mergedClaimsAndCapabilities = MergeClaimsIntoCapabilityJson(claims, capabilitiesJson); + try + { + JObject capabilitiesJson = CreateClientCapabilitiesRequestJson(clientCapabilities); + JObject mergedClaimsAndCapabilities = MergeClaimsIntoCapabilityJson(claims, capabilitiesJson); - return JsonHelper.JsonObjectToString(mergedClaimsAndCapabilities); + return JsonHelper.JsonObjectToString(mergedClaimsAndCapabilities); + } + catch (PlatformNotSupportedException pns) + { + throw CreateJsonEncoderException(pns); + } + catch (TypeInitializationException tie) when (tie.InnerException is PlatformNotSupportedException) + { + throw CreateJsonEncoderException(tie.InnerException as PlatformNotSupportedException); + } } return claims; @@ -91,5 +104,18 @@ private static JObject CreateClientCapabilitiesRequestJson(IEnumerable c } }; } + + private static MsalClientException CreateJsonEncoderException(PlatformNotSupportedException innerException) + { + string processArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + bool is64BitProcess = Environment.Is64BitProcess; + string hwIntrinsicEnvValue = Environment.GetEnvironmentVariable("DOTNET_EnableHWIntrinsic") + ?? Environment.GetEnvironmentVariable("COMPlus_EnableHWIntrinsic"); + + return new MsalClientException( + MsalError.JsonEncoderIntrinsicsUnsupported, + MsalErrorMessage.JsonEncoderIntrinsicsUnsupported(processArchitecture, is64BitProcess, hwIntrinsicEnvValue), + innerException); + } } } diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 4bcc576a42..af81458e59 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -841,6 +841,12 @@ public static class MsalError /// public const string InvalidJsonClaimsFormat = "invalid_json_claims_format"; + /// + /// What happens?The JSON encoder failed due to unavailable hardware intrinsics (SIMD/SSSE3). This typically occurs on 32-bit processes or systems with hardware intrinsics disabled. + /// MitigationRun the process as 64-bit, update the runtime, or set the environment variable DOTNET_EnableHWIntrinsic=0 to force the non-SIMD code path. + /// + public const string JsonEncoderIntrinsicsUnsupported = "json_encoder_intrinsics_unsupported"; + /// /// What happens?The authority configured at the application level is different than the authority configured at the request level /// MitigationEnsure the same authority type is used diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index 85bb9d74cc..34c7629f27 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -445,5 +445,14 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string ForceRefreshAndTokenHasNotCompatible = "Cannot specify ForceRefresh and AccessTokenSha256ToRefresh in the same request."; public const string RequestTimeOut = "Request to the endpoint timed out."; public const string MalformedOidcAuthorityFormat = "Possible cause: When using Entra External ID, you didn't append /v2.0, for example {0}/v2.0\""; + + public static string JsonEncoderIntrinsicsUnsupported(string processArchitecture, bool is64BitProcess, string hwIntrinsicEnvValue) + { + return $"JSON encoding failed due to unavailable hardware intrinsics (SIMD/SSSE3). " + + $"Process architecture: {processArchitecture}, " + + $"Is 64-bit process: {is64BitProcess}, " + + $"DOTNET_EnableHWIntrinsic: {hwIntrinsicEnvValue ?? "(not set)"}. " + + "Mitigation: Run as 64-bit process, update runtime, or set environment variable DOTNET_EnableHWIntrinsic=0 to force the non-SIMD code path."; + } } } diff --git a/src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs b/src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs index 55ee2a2f15..39465ab365 100644 --- a/src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs +++ b/src/client/Microsoft.Identity.Client/Utils/JsonHelper.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Internal; @@ -145,7 +147,21 @@ internal static long ExtractParsedIntOrZero(JObject json, string key) } #if SUPPORTS_SYSTEM_TEXT_JSON - internal static string JsonObjectToString(JsonObject jsonObject) => jsonObject.ToJsonString(); + internal static string JsonObjectToString(JsonObject jsonObject) + { + try + { + return jsonObject.ToJsonString(); + } + catch (PlatformNotSupportedException pns) + { + throw CreateJsonEncoderException(pns); + } + catch (TypeInitializationException tie) when (tie.InnerException is PlatformNotSupportedException) + { + throw CreateJsonEncoderException(tie.InnerException as PlatformNotSupportedException); + } + } internal static JsonObject ParseIntoJsonObject(string json) => JsonNode.Parse(json).AsObject(); @@ -168,23 +184,34 @@ internal static long ExtractParsedIntOrZero(JObject json, string key) /// internal static JObject Merge(JObject originalJson, JObject newContent) { - // Output buffer to store the merged JSON - var outputBuffer = new ArrayBufferWriter(); - - // Parse the original and new JSON content - using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson.ToJsonString())) - using (JsonDocument jDoc2 = JsonDocument.Parse(newContent.ToJsonString())) - using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true })) + try { - // Merge the JSON elements - MergeJsonElements(jsonWriter, jDoc1.RootElement, jDoc2.RootElement); - } + // Output buffer to store the merged JSON + var outputBuffer = new ArrayBufferWriter(); - // Convert the merged JSON to a UTF-8 encoded string - string mergedJsonString = Encoding.UTF8.GetString(outputBuffer.WrittenSpan); + // Parse the original and new JSON content + using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson.ToJsonString())) + using (JsonDocument jDoc2 = JsonDocument.Parse(newContent.ToJsonString())) + using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true })) + { + // Merge the JSON elements + MergeJsonElements(jsonWriter, jDoc1.RootElement, jDoc2.RootElement); + } + + // Convert the merged JSON to a UTF-8 encoded string + string mergedJsonString = Encoding.UTF8.GetString(outputBuffer.WrittenSpan); - // Parse the merged JSON string to a JObject - return ParseIntoJsonObject(mergedJsonString); + // Parse the merged JSON string to a JObject + return ParseIntoJsonObject(mergedJsonString); + } + catch (PlatformNotSupportedException pns) + { + throw CreateJsonEncoderException(pns); + } + catch (TypeInitializationException tie) when (tie.InnerException is PlatformNotSupportedException) + { + throw CreateJsonEncoderException(tie.InnerException as PlatformNotSupportedException); + } } // Merges two JSON elements based on their value kind @@ -284,6 +311,19 @@ private static void MergeArrays(Utf8JsonWriter jsonWriter, JsonElement root1, Js // End writing the merged array jsonWriter.WriteEndArray(); } + + private static MsalClientException CreateJsonEncoderException(PlatformNotSupportedException innerException) + { + string processArchitecture = RuntimeInformation.ProcessArchitecture.ToString(); + bool is64BitProcess = Environment.Is64BitProcess; + string hwIntrinsicEnvValue = Environment.GetEnvironmentVariable("DOTNET_EnableHWIntrinsic") + ?? Environment.GetEnvironmentVariable("COMPlus_EnableHWIntrinsic"); + + return new MsalClientException( + MsalError.JsonEncoderIntrinsicsUnsupported, + MsalErrorMessage.JsonEncoderIntrinsicsUnsupported(processArchitecture, is64BitProcess, hwIntrinsicEnvValue), + innerException); + } #else internal static string JsonObjectToString(JObject jsonObject) => jsonObject.ToString(Formatting.None); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/ClaimsTest.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/ClaimsTest.cs index 0a013f4b90..56689ff1c3 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/ClaimsTest.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/OAuth2Tests/ClaimsTest.cs @@ -249,5 +249,38 @@ public void ClaimsMerge_Test(string claims, string[] capabilities, string expect var mergedJson = ClaimsHelper.GetMergedClaimsAndClientCapabilities(claims, capabilities); Assert.AreEqual(expectedMergedJson, mergedJson); } + + [TestMethod] + public void ClaimsHelper_HandlesPlatformNotSupportedException_FromJsonEncoder() + { + // This test verifies that PlatformNotSupportedException thrown during JSON encoding + // is caught and wrapped in MsalClientException with proper diagnostic information. + + // Note: We cannot easily simulate a real PlatformNotSupportedException from System.Text.Json + // without running on an unsupported platform, so this test verifies the error handling + // code compiles and the error constants are properly defined. + + // Verify error code constant exists + Assert.AreEqual("json_encoder_intrinsics_unsupported", MsalError.JsonEncoderIntrinsicsUnsupported); + + // Verify error message method generates a proper message + string errorMsg = MsalErrorMessage.JsonEncoderIntrinsicsUnsupported("X64", true, "0"); + Assert.IsTrue(errorMsg.Contains("JSON encoding failed")); + Assert.IsTrue(errorMsg.Contains("X64")); + Assert.IsTrue(errorMsg.Contains("Is 64-bit process: True")); + Assert.IsTrue(errorMsg.Contains("DOTNET_EnableHWIntrinsic: 0")); + Assert.IsTrue(errorMsg.Contains("SIMD")); + } + + [TestMethod] + public void ClaimsHelper_ErrorMessage_HandlesNullEnvVariable() + { + // Test that the error message handles null environment variable correctly + string errorMsg = MsalErrorMessage.JsonEncoderIntrinsicsUnsupported("X86", false, null); + Assert.IsTrue(errorMsg.Contains("JSON encoding failed")); + Assert.IsTrue(errorMsg.Contains("X86")); + Assert.IsTrue(errorMsg.Contains("Is 64-bit process: False")); + Assert.IsTrue(errorMsg.Contains("DOTNET_EnableHWIntrinsic: (not set)")); + } } }