|
10 | 10 | using System.Net.Http; |
11 | 11 | using System.Security.Cryptography; |
12 | 12 | using System.Security.Cryptography.X509Certificates; |
| 13 | +using System.Text.Json; |
13 | 14 | using System.Threading; |
14 | 15 | using System.Threading.Tasks; |
15 | 16 | using Microsoft.Identity.Client; |
16 | | -using Microsoft.Identity.Client.Cache.Keys; |
17 | 17 | using Microsoft.Identity.Client.Internal; |
| 18 | +using Microsoft.Identity.Client.OAuth2; |
18 | 19 | using Microsoft.Identity.Client.PlatformsCommon.Shared; |
| 20 | +using Microsoft.Identity.Client.RP; |
19 | 21 | using Microsoft.Identity.Client.Utils; |
20 | 22 | using Microsoft.Identity.Test.Common.Core.Helpers; |
21 | 23 | using Microsoft.Identity.Test.Common.Core.Mocks; |
22 | 24 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
23 | | -using static Microsoft.Identity.Client.Internal.JsonWebToken; |
24 | | -using Microsoft.Identity.Client.RP; |
25 | | -using Microsoft.Identity.Client.Http; |
26 | | -using Microsoft.Identity.Client.OAuth2; |
27 | 25 |
|
28 | 26 | namespace Microsoft.Identity.Test.Unit |
29 | 27 | { |
@@ -648,7 +646,6 @@ public void ClientAssertionTests(bool sendX5C, bool useSha2AndPss, bool addExtra |
648 | 646 | "MIIDQjCCAiqgAwIBAgIQTuexEO9cdYhC0jy1nmS6jTANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBd0cndhbGtlLm9ubWljcm9zb2Z0LmNvbTAeFw0xNzA4MTExODEzMTBaFw0xODA4MTExODMzMTBaMCIxIDAeBgNVBAMMF3Ryd2Fsa2Uub25taWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Qe3Ah/E97K0o288gYUNa0H8FO/w8pb1dvls/boQDoZxUD11TpAQrKZwstS6+ulGF6cHmj44AH8MNBKNUbW2L1NTjFG9bltaSXpJXzbIH/cUppF9rxngZ0CM7cHtuoccBPBVEuQiJ86pD7qlqE2EA2BdBmfz3Hd41rybdaWkHMxMcBC7nh6w87/KoyikKXCMLUUyRTJLSivo+gfKJsiYGAjqZ54aJraP5LMiPG2qYTOZR6wMme93mYRp85sqGTvgzRCq37STH2HmcYilUQ9kZFe5SR+1vOki97XLg+H7FuFtkSMM7dEnTWkDv+BJ1ZQvCEj623cJxXlq0fd7hVUxIQIDAQABo3QwcjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMCIGA1UdEQQbMBmCF3Ryd2Fsa2Uub25taWNyb3NvZnQuY29tMB0GA1UdDgQWBBSauRo9cNk8J6RTLWMQSyUQnxjQzDANBgkqhkiG9w0BAQsFAAOCAQEAhYl1I8qETtvVt6m/YrGknA90R/FtIePt/ViBae3mxPJWlVoq5fTTriQcuPHXfI5kbjTQJIwCVTT/CRSlKkzRcrSsQUxxHNE7IdpvvDbkf6AMPxQhNACHQd0cIWmsmf+ItKsC70LKQ+93+VgmBsv2j8XwF0JTqwuKoqXnDjCzHvmU67xhPY6CSPA/0XOiVTx1BDWd5cPdsH2bZnAeApsvrzU8W7iPgV/oN9MMfogocvDUXd6T+QGLMAYoInHXsqG6+SEarqRDUPQZOHo5Ax4Mvhsnd2b4u5d5Y/R0z0wUwtOiF0Tu+w79JIqDRYaaJLTKxZ+2DyYOu54u0LGsGhki1g==", |
649 | 647 | decodedToken.Header["x5c"]); |
650 | 648 | } |
651 | | - |
652 | 649 | } |
653 | 650 |
|
654 | 651 | private static void AssertClientAssertionHeader( |
@@ -1057,6 +1054,64 @@ public async Task AcquireTokenForClient_ShouldSendClientInfoParameter_WithValueT |
1057 | 1054 | } |
1058 | 1055 | } |
1059 | 1056 |
|
| 1057 | + [TestMethod] |
| 1058 | + public async Task ClientAssertionWithComplexClaims() |
| 1059 | + { |
| 1060 | + using (var harness = CreateTestHarness()) |
| 1061 | + { |
| 1062 | + harness.HttpManager.AddInstanceDiscoveryMockHandler(); |
| 1063 | + var certificate = CertHelper.GetOrCreateTestCert(); |
| 1064 | + var exportedCertificate = Convert.ToBase64String(certificate.Export(X509ContentType.Cert)); |
| 1065 | + |
| 1066 | + // Test complex nested claims as described in the issue |
| 1067 | + IDictionary<string, string> extraAssertionContent = new Dictionary<string, string> |
| 1068 | + { |
| 1069 | + { "foo", "bar" }, |
| 1070 | + { "custom_claims", "{\"xms_foo\":[\"abc\",\"def\"],\"xms_az_foo\":\"bar\"}" } |
| 1071 | + }; |
| 1072 | + |
| 1073 | + var cca = ConfidentialClientApplicationBuilder |
| 1074 | + .Create(TestConstants.ClientId) |
| 1075 | + .WithAuthority("https://login.microsoftonline.com/tid") |
| 1076 | + .WithHttpManager(harness.HttpManager) |
| 1077 | + .WithClientClaims(certificate, extraAssertionContent, mergeWithDefaultClaims: true, sendX5C: true) |
| 1078 | + .Build(); |
| 1079 | + |
| 1080 | + var handler = harness.HttpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials); |
| 1081 | + JwtSecurityToken assertion = null; |
| 1082 | + handler.AdditionalRequestValidation = (r) => |
| 1083 | + { |
| 1084 | + var requestContent = r.Content.ReadAsStringAsync().GetAwaiter().GetResult(); |
| 1085 | + var formsData = CoreHelpers.ParseKeyValueList(requestContent, '&', true, null); |
| 1086 | + |
| 1087 | + // Check presence of client_assertion in request |
| 1088 | + Assert.IsTrue(formsData.TryGetValue("client_assertion", out string encodedJwt), "Missing client_assertion from request"); |
| 1089 | + |
| 1090 | + // Decode and validate the JWT |
| 1091 | + var jwtHandler = new JwtSecurityTokenHandler(); |
| 1092 | + assertion = jwtHandler.ReadJwtToken(encodedJwt); |
| 1093 | + |
| 1094 | + // Validate that custom_claims is a nested object, not an escaped string |
| 1095 | + var customClaimsClaim = assertion.Claims.FirstOrDefault(c => c.Type == "custom_claims"); |
| 1096 | + Assert.IsNotNull(customClaimsClaim, "custom_claims should be present"); |
| 1097 | + |
| 1098 | + // Validate the type of claim value is a string |
| 1099 | + Assert.IsTrue(typeof(string).IsAssignableFrom(customClaimsClaim.Value.GetType()), "custom_claims claim value should be a string"); |
| 1100 | + |
| 1101 | + // The value should be a JSON object, not an escaped string |
| 1102 | + string customClaimsValue = customClaimsClaim.Value; |
| 1103 | + var jsonElement = JsonSerializer.Deserialize<JsonElement>(customClaimsValue); // This will throw if not valid JSON object |
| 1104 | + Assert.AreEqual(JsonValueKind.Object, jsonElement.ValueKind, "custom_claims claim value should be a JSON object"); |
| 1105 | + }; |
| 1106 | + |
| 1107 | + AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope) |
| 1108 | + .ExecuteAsync() |
| 1109 | + .ConfigureAwait(false); |
| 1110 | + |
| 1111 | + Assert.IsNotNull(result.AccessToken); |
| 1112 | + } |
| 1113 | + } |
| 1114 | + |
1060 | 1115 | private void BeforeCacheAccess(TokenCacheNotificationArgs args) |
1061 | 1116 | { |
1062 | 1117 | args.TokenCache.DeserializeMsalV3(_serializedCache); |
|
0 commit comments