Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,18 @@
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Cache.Keys;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using Microsoft.Identity.Client.RP;
using Microsoft.Identity.Client.Utils;
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.Identity.Test.Common.Core.Mocks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static Microsoft.Identity.Client.Internal.JsonWebToken;
using Microsoft.Identity.Client.RP;
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.OAuth2;

namespace Microsoft.Identity.Test.Unit
{
Expand Down Expand Up @@ -648,7 +646,6 @@ public void ClientAssertionTests(bool sendX5C, bool useSha2AndPss, bool addExtra
"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==",
decodedToken.Header["x5c"]);
}

}

private static void AssertClientAssertionHeader(
Expand Down Expand Up @@ -1057,6 +1054,64 @@ public async Task AcquireTokenForClient_ShouldSendClientInfoParameter_WithValueT
}
}

[TestMethod]
public async Task ClientAssertionWithComplexClaims()
{
using (var harness = CreateTestHarness())
{
harness.HttpManager.AddInstanceDiscoveryMockHandler();
var certificate = CertHelper.GetOrCreateTestCert();
var exportedCertificate = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));

// Test complex nested claims as described in the issue
IDictionary<string, string> extraAssertionContent = new Dictionary<string, string>
{
{ "foo", "bar" },
{ "custom_claims", "{\"xms_foo\":[\"abc\",\"def\"],\"xms_az_foo\":\"bar\"}" }
};

var cca = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithAuthority("https://login.microsoftonline.com/tid")
.WithHttpManager(harness.HttpManager)
.WithClientClaims(certificate, extraAssertionContent, mergeWithDefaultClaims: true, sendX5C: true)
.Build();

var handler = harness.HttpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials);
JwtSecurityToken assertion = null;
handler.AdditionalRequestValidation = (r) =>
{
var requestContent = r.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var formsData = CoreHelpers.ParseKeyValueList(requestContent, '&', true, null);

// Check presence of client_assertion in request
Assert.IsTrue(formsData.TryGetValue("client_assertion", out string encodedJwt), "Missing client_assertion from request");

// Decode and validate the JWT
var jwtHandler = new JwtSecurityTokenHandler();
assertion = jwtHandler.ReadJwtToken(encodedJwt);

// Validate that custom_claims is a nested object, not an escaped string
var customClaimsClaim = assertion.Claims.FirstOrDefault(c => c.Type == "custom_claims");
Assert.IsNotNull(customClaimsClaim, "custom_claims should be present");

// Validate the type of claim value is a string
Assert.IsTrue(typeof(string).IsAssignableFrom(customClaimsClaim.Value.GetType()), "custom_claims claim value should be a string");

// The value should be a JSON object, not an escaped string
string customClaimsValue = customClaimsClaim.Value;
var jsonElement = JsonSerializer.Deserialize<JsonElement>(customClaimsValue); // This will throw if not valid JSON object
Assert.AreEqual(JsonValueKind.Object, jsonElement.ValueKind, "custom_claims claim value should be a JSON object");
};

AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result.AccessToken);
}
}

private void BeforeCacheAccess(TokenCacheNotificationArgs args)
{
args.TokenCache.DeserializeMsalV3(_serializedCache);
Expand Down