From 52a4b31f6a5a20eb11f149ce959137dd0cecb6ba Mon Sep 17 00:00:00 2001 From: marcinzo Date: Mon, 24 Nov 2025 09:14:54 -0800 Subject: [PATCH 01/12] Add support for calling WithClientClaims flow for token acquisition --- ...entialClientApplicationBuilderExtension.cs | 19 +- .../net8.0/InternalAPI.Unshipped.txt | 2 + .../TokenAcquisition.cs | 38 +- .../TokenAcquisitionAuthorityTests.cs | 353 ++++++++++++++++++ 4 files changed, 401 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs index 324f0e458..4da108df1 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs @@ -25,12 +25,15 @@ public static ConfidentialClientApplicationBuilder WithClientCredentials( credentialSourceLoaderParameters).GetAwaiter().GetResult(); } +#pragma warning disable RS0051 public static async Task WithClientCredentialsAsync( this ConfidentialClientApplicationBuilder builder, IEnumerable clientCredentials, ILogger logger, ICredentialsLoader credentialsLoader, - CredentialSourceLoaderParameters? credentialSourceLoaderParameters) + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + IDictionary? clientClaims = null) +#pragma warning restore RS0051 { var credential = await LoadCredentialForMsalOrFailAsync( clientCredentials, @@ -49,7 +52,7 @@ public static async Task WithClientCredent case CredentialType.SignedAssertion: return builder.WithClientAssertion((credential.CachedValue as ClientAssertionProviderBase)!.GetSignedAssertionAsync); case CredentialType.Certificate: - return builder.WithCertificate(credential.Certificate); + return builder.WithCertificateInternal(credential, clientClaims); case CredentialType.Secret: return builder.WithClientSecret(credential.ClientSecret); default: @@ -58,6 +61,18 @@ public static async Task WithClientCredent } } + private static ConfidentialClientApplicationBuilder WithCertificateInternal( + this ConfidentialClientApplicationBuilder builder, + CredentialDescription credentialDescription, + IDictionary? clientClaims = null) + { + if (clientClaims != null && clientClaims.Count > 0) + { + return builder.WithClientClaims(credentialDescription.Certificate, clientClaims); + } + return builder.WithCertificate(credentialDescription.Certificate); + } + internal /* for test */ async static Task LoadCredentialForMsalOrFailAsync( IEnumerable clientCredentials, ILogger logger, diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 7dc5c5811..0594a5b04 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Collections.Generic.IDictionary? clientClaims = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 2513b3270..66d05d304 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -210,16 +210,22 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn } } - /// /// Allows creation of confidential client applications targeting regional and global authorities /// when supporting managed identities. /// - /// Merged configuration options + /// Merged configuration options. + /// Optional client claims. /// Concatenated string of authority, cliend id and azure region - private static string GetApplicationKey(MergedOptions mergedOptions) + private static string GetApplicationKey(MergedOptions mergedOptions, IDictionary? clientClaims = null) { string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty()); + + if (clientClaims != null) + { + credentialId += "-" + string.Join("-", clientClaims.Select(kvp => $"{kvp.Key}:{kvp.Value}")); + } + return DefaultTokenAcquirerFactoryImplementation.GetKey(mergedOptions.Authority, mergedOptions.ClientId, mergedOptions.AzureRegion) + credentialId; } @@ -258,7 +264,6 @@ public async Task GetAuthenticationResultForUserAsync( _ = Throws.IfNull(scopes); MergedOptions mergedOptions = GetMergedOptions(authenticationScheme, tokenAcquisitionOptions); - user ??= await _tokenAcquisitionHost.GetAuthenticatedUserAsync(user).ConfigureAwait(false); var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); @@ -896,11 +901,25 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti ); } + private static IDictionary? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) + { + IDictionary? clientClaims = null; + if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && + tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] is not null) + { + clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as IDictionary; + } + return clientClaims; + } +#pragma warning disable RS0051 // Add internal types and members to the declared API internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync( - MergedOptions mergedOptions) +#pragma warning restore RS0051 // Add internal types and members to the declared API + MergedOptions mergedOptions, + TokenAcquisitionOptions? tokenAcquisitionOptions = null) // just for PoC will drive this through MergedOptions later { - string key = GetApplicationKey(mergedOptions); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + string key = GetApplicationKey(mergedOptions, clientClaims); // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680 // Fast path: check if already created @@ -918,7 +937,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return app; // Build and store the application - var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions); + var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions, clientClaims); // Recompute the key as BuildConfidentialClientApplicationAsync can cause it to change. key = GetApplicationKey(mergedOptions); @@ -934,7 +953,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti /// /// Creates an MSAL confidential client application. /// - private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions) + private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions, IDictionary? clientClaims) { mergedOptions.PrepareAuthorityInstanceForMsal(); @@ -991,7 +1010,8 @@ await builder.WithClientCredentialsAsync( mergedOptions.ClientCredentials!, _logger, _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)); + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), + clientClaims); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) { diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index a516dd240..c35c45081 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -539,5 +539,358 @@ public async Task GetOrBuildManagedIdentity_TestConcurrencyAsync(string? clientI Assert.Same(testApp, app); } } + + [Fact] + public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsync() + { + // Arrange + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + $"CN=TestClaimsCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + + var customClaims = new Dictionary + { + { "custom_claim_one", "value_one" }, + { "custom_claim_two", "value_two" } + }; + + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary + { + { "IDWEB_CLIENT_CLAIMS", customClaims } + } + }; + + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + + // Build service collection + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + + InitializeTokenAcquisitionObjects(); + + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + + // Act first token acquisition (network call expected) + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + var result = await _tokenAcquisition.GetAuthenticationResultForAppAsync( + scope: "https://graph.microsoft.com/.default", + authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, + tenant: tenantId, + tokenAcquisitionOptions: null); + + // Assert first network call produced client assertion with claims + Assert.NotNull(result.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payloadJson = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("value_one", payloadJson, StringComparison.Ordinal); + Assert.Contains("value_two", payloadJson, StringComparison.Ordinal); + + // Second call should be served from cache: no new network request, no new assertion captured + capturingHandler.ResetCapture(); + var result2 = await _tokenAcquisition.GetAuthenticationResultForAppAsync( + scope: "https://graph.microsoft.com/.default", + authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, + tenant: tenantId, + tokenAcquisitionOptions: null); + Assert.NotNull(result2.AccessToken); + Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + } + + [Fact] + public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() + { + // Arrange: initial build with claims + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionACacheCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var customClaims = new Dictionary + { + { "custom_claim_one", "value_one" }, + { "custom_claim_two", "value_two" } + }; + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + Assert.NotNull(first.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payloadJson = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("value_one", payloadJson, StringComparison.Ordinal); + capturingHandler.ResetCapture(); + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + Assert.NotNull(second.AccessToken); + // Option A expectation: cached token => no new client_assertion sent + Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + Assert.Equal(first.AccessToken, second.AccessToken); // token from cache + } + + [Fact] + public async Task ClientClaims_ForceRefresh_NewAssertionAsync() + { + // Arrange similar to Option A + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionBForceRefreshCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var customClaims = new Dictionary { { "claimX", "claimXValue" } }; + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + Assert.NotNull(first.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var firstAssertion = capturingHandler.CapturedClientAssertion; + capturingHandler.ResetCapture(); + var forceOptions = new TokenAcquisitionOptions { ForceRefresh = true }; // Option B + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, forceOptions); + Assert.NotNull(second.AccessToken); + // New network call expected (assertion recaptured) + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payload2 = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("claimXValue", payload2, StringComparison.Ordinal); + // But assertions should differ (signed each time by MSAL with new exp etc.) + Assert.NotEqual(firstAssertion, capturingHandler.CapturedClientAssertion); + } + + [Fact] + public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() + { + // Arrange initial app with initial claims + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionCChangedClaimsCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var initialClaims = new Dictionary { { "c1", "v1" } }; + var initialOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", initialClaims } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, initialOptions); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var firstPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("v1", firstPayload, StringComparison.Ordinal); + // Attempt to change claims (should not affect cached app) + var newClaims = new Dictionary { { "c1", "v2" }, { "c2", "vNew" } }; + var newOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", newClaims } } + }; + // Call GetOrBuild again with new claims + var app2 = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, newOptions); + // Same instance expected + Assert.Same(app2, await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, null)); + capturingHandler.ResetCapture(); + var forceRefresh = new TokenAcquisitionOptions { ForceRefresh = true }; // network call + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, forceRefresh); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var secondPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + // Validate old value still present and new ones absent + Assert.Contains("v1", secondPayload, StringComparison.Ordinal); + Assert.DoesNotContain("v2", secondPayload, StringComparison.Ordinal); + Assert.DoesNotContain("vNew", secondPayload, StringComparison.Ordinal); + } + + private static string DecodeJwtPayload(string jwt) + { + var parts = jwt.Split('.'); + Assert.True(parts.Length >= 2, "JWT format invalid"); + string payload = parts[1]; + // Base64Url decode + string padded = payload.Replace('-', '+').Replace('_', '/'); + switch (padded.Length % 4) + { + case 2: padded += "=="; break; + case 3: padded += "="; break; + } + var bytes = Convert.FromBase64String(padded); + return System.Text.Encoding.UTF8.GetString(bytes); + } + + private class CapturingMsalHttpClientFactory : IMsalHttpClientFactory + { + private readonly HttpClient _httpClient; + public CapturingMsalHttpClientFactory(HttpClient httpClient) => _httpClient = httpClient; + public HttpClient GetHttpClient() => _httpClient; + } + + private class CapturingHandler : HttpMessageHandler + { + private readonly string _authorityBase; + public string? CapturedClientAssertion { get; private set; } + public CapturingHandler(string authorityBase) => _authorityBase = authorityBase.TrimEnd('/'); + public void ResetCapture() => CapturedClientAssertion = null; + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var uri = request.RequestUri!.ToString(); + if (uri.EndsWith("/.well-known/openid-configuration", StringComparison.OrdinalIgnoreCase)) + { + var json = $"{{ \"token_endpoint\": \"{_authorityBase}/oauth2/v2.0/token\", \"issuer\": \"{_authorityBase}/\" }}"; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") + }; + } + if (uri.EndsWith("/oauth2/v2.0/token", StringComparison.OrdinalIgnoreCase)) + { + var body = await request.Content!.ReadAsStringAsync(); + foreach (var kv in body.Split('&')) + { + var pair = kv.Split('='); + if (pair.Length == 2 && pair[0] == "client_assertion") + { + CapturedClientAssertion = Uri.UnescapeDataString(pair[1]); + } + } + var tokenResponse = "{ \"access_token\": \"at\", \"expires_in\": 3600, \"token_type\": \"Bearer\" }"; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(tokenResponse, System.Text.Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound); + } + } } } From aaac166fdfbf132df6c0dead605b7bbd88c25850 Mon Sep 17 00:00:00 2001 From: trwalke Date: Sun, 7 Dec 2025 19:26:05 -0800 Subject: [PATCH 02/12] Updating client claims to use abstractions instead --- .../MicrosoftIdentityOptions.cs | 8 +++++ .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 2 ++ .../TokenAcquisition.cs | 30 ++++--------------- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs index 04a841160..d4362419d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs @@ -213,5 +213,13 @@ internal bool HasClientCredentials /// Sets query parameters for the query string in the HTTP request to the IdP. /// public IDictionary? ExtraQueryParameters { get; set; } + + /// + /// Gets or sets the claims used to create the client assertion for authentication. + /// + /// The client assertion claims are typically used in scenarios where client + /// authentication requires a signed JWT (JSON Web Token). Ensure the claims are properly formatted and encoded + /// as a JSON string before setting this property. + public IDictionary? ClientAssertionClaims { get; set; } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index 13496576f..fe6ea998f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index 13496576f..fe6ea998f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 13496576f..fe6ea998f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 13496576f..fe6ea998f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 13496576f..fe6ea998f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? +Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 66d05d304..fa184e42e 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -215,17 +215,11 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn /// when supporting managed identities. /// /// Merged configuration options. - /// Optional client claims. /// Concatenated string of authority, cliend id and azure region - private static string GetApplicationKey(MergedOptions mergedOptions, IDictionary? clientClaims = null) + private static string GetApplicationKey(MergedOptions mergedOptions) { string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty()); - if (clientClaims != null) - { - credentialId += "-" + string.Join("-", clientClaims.Select(kvp => $"{kvp.Key}:{kvp.Value}")); - } - return DefaultTokenAcquirerFactoryImplementation.GetKey(mergedOptions.Authority, mergedOptions.ClientId, mergedOptions.AzureRegion) + credentialId; } @@ -745,6 +739,7 @@ private MergedOptions GetMergedOptions(string? authenticationScheme, TokenAcquis Instance = microsoftEntraApplicationOptions.Instance ?? parentMergedOptions.Instance, AzureRegion = microsoftEntraApplicationOptions.AzureRegion ?? parentMergedOptions.AzureRegion, TenantId = microsoftEntraApplicationOptions.TenantId ?? parentMergedOptions.TenantId, + ClientAssertionClaims = microsoftEntraApplicationOptions.ClientAssertionClaims ?? parentMergedOptions.ClientAssertionClaims }; } else @@ -901,25 +896,13 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti ); } - private static IDictionary? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) - { - IDictionary? clientClaims = null; - if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && - tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] is not null) - { - clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as IDictionary; - } - return clientClaims; - } - #pragma warning disable RS0051 // Add internal types and members to the declared API internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync( #pragma warning restore RS0051 // Add internal types and members to the declared API MergedOptions mergedOptions, TokenAcquisitionOptions? tokenAcquisitionOptions = null) // just for PoC will drive this through MergedOptions later { - var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); - string key = GetApplicationKey(mergedOptions, clientClaims); + string key = GetApplicationKey(mergedOptions); // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680 // Fast path: check if already created @@ -937,7 +920,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return app; // Build and store the application - var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions, clientClaims); + var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions); // Recompute the key as BuildConfidentialClientApplicationAsync can cause it to change. key = GetApplicationKey(mergedOptions); @@ -953,7 +936,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti /// /// Creates an MSAL confidential client application. /// - private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions, IDictionary? clientClaims) + private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions) { mergedOptions.PrepareAuthorityInstanceForMsal(); @@ -1010,8 +993,7 @@ await builder.WithClientCredentialsAsync( mergedOptions.ClientCredentials!, _logger, _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), - clientClaims); + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) { From eece814c2dcc4bea71bcbbb1df9ac809c55112fc Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 20 Jan 2026 14:47:38 -0800 Subject: [PATCH 03/12] Revert "Updating client claims to use abstractions instead" This reverts commit aaac166fdfbf132df6c0dead605b7bbd88c25850. --- .../MicrosoftIdentityOptions.cs | 8 ----- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 -- .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 2 -- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 -- .../TokenAcquisition.cs | 30 +++++++++++++++---- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs index d4362419d..04a841160 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityOptions.cs @@ -213,13 +213,5 @@ internal bool HasClientCredentials /// Sets query parameters for the query string in the HTTP request to the IdP. /// public IDictionary? ExtraQueryParameters { get; set; } - - /// - /// Gets or sets the claims used to create the client assertion for authentication. - /// - /// The client assertion claims are typically used in scenarios where client - /// authentication requires a signed JWT (JSON Web Token). Ensure the claims are properly formatted and encoded - /// as a JSON string before setting this property. - public IDictionary? ClientAssertionClaims { get; set; } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index fe6ea998f..13496576f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -9,8 +9,6 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index fe6ea998f..13496576f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -9,8 +9,6 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index fe6ea998f..13496576f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -9,8 +9,6 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index fe6ea998f..13496576f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -9,8 +9,6 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index fe6ea998f..13496576f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -9,8 +9,6 @@ Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.get -> System.Collections.Generic.IDictionary? -Microsoft.Identity.Web.MicrosoftIdentityOptions.ClientAssertionClaims.set -> void override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index fa184e42e..66d05d304 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -215,11 +215,17 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn /// when supporting managed identities. /// /// Merged configuration options. + /// Optional client claims. /// Concatenated string of authority, cliend id and azure region - private static string GetApplicationKey(MergedOptions mergedOptions) + private static string GetApplicationKey(MergedOptions mergedOptions, IDictionary? clientClaims = null) { string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty()); + if (clientClaims != null) + { + credentialId += "-" + string.Join("-", clientClaims.Select(kvp => $"{kvp.Key}:{kvp.Value}")); + } + return DefaultTokenAcquirerFactoryImplementation.GetKey(mergedOptions.Authority, mergedOptions.ClientId, mergedOptions.AzureRegion) + credentialId; } @@ -739,7 +745,6 @@ private MergedOptions GetMergedOptions(string? authenticationScheme, TokenAcquis Instance = microsoftEntraApplicationOptions.Instance ?? parentMergedOptions.Instance, AzureRegion = microsoftEntraApplicationOptions.AzureRegion ?? parentMergedOptions.AzureRegion, TenantId = microsoftEntraApplicationOptions.TenantId ?? parentMergedOptions.TenantId, - ClientAssertionClaims = microsoftEntraApplicationOptions.ClientAssertionClaims ?? parentMergedOptions.ClientAssertionClaims }; } else @@ -896,13 +901,25 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti ); } + private static IDictionary? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) + { + IDictionary? clientClaims = null; + if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && + tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] is not null) + { + clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as IDictionary; + } + return clientClaims; + } + #pragma warning disable RS0051 // Add internal types and members to the declared API internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync( #pragma warning restore RS0051 // Add internal types and members to the declared API MergedOptions mergedOptions, TokenAcquisitionOptions? tokenAcquisitionOptions = null) // just for PoC will drive this through MergedOptions later { - string key = GetApplicationKey(mergedOptions); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + string key = GetApplicationKey(mergedOptions, clientClaims); // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680 // Fast path: check if already created @@ -920,7 +937,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return app; // Build and store the application - var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions); + var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions, clientClaims); // Recompute the key as BuildConfidentialClientApplicationAsync can cause it to change. key = GetApplicationKey(mergedOptions); @@ -936,7 +953,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti /// /// Creates an MSAL confidential client application. /// - private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions) + private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions, IDictionary? clientClaims) { mergedOptions.PrepareAuthorityInstanceForMsal(); @@ -993,7 +1010,8 @@ await builder.WithClientCredentialsAsync( mergedOptions.ClientCredentials!, _logger, _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)); + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), + clientClaims); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) { From cc8a868890051c3a05dfb4ed8c49d4a67ce0b9a5 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 20 Jan 2026 20:25:09 -0800 Subject: [PATCH 04/12] Adding WithExtraClientAssertionClaims --- ...entialClientApplicationBuilderExtension.cs | 19 +------- .../net8.0/InternalAPI.Unshipped.txt | 2 - .../TokenAcquisition.cs | 45 ++++++++++++++----- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs index 4da108df1..324f0e458 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs @@ -25,15 +25,12 @@ public static ConfidentialClientApplicationBuilder WithClientCredentials( credentialSourceLoaderParameters).GetAwaiter().GetResult(); } -#pragma warning disable RS0051 public static async Task WithClientCredentialsAsync( this ConfidentialClientApplicationBuilder builder, IEnumerable clientCredentials, ILogger logger, ICredentialsLoader credentialsLoader, - CredentialSourceLoaderParameters? credentialSourceLoaderParameters, - IDictionary? clientClaims = null) -#pragma warning restore RS0051 + CredentialSourceLoaderParameters? credentialSourceLoaderParameters) { var credential = await LoadCredentialForMsalOrFailAsync( clientCredentials, @@ -52,7 +49,7 @@ public static async Task WithClientCredent case CredentialType.SignedAssertion: return builder.WithClientAssertion((credential.CachedValue as ClientAssertionProviderBase)!.GetSignedAssertionAsync); case CredentialType.Certificate: - return builder.WithCertificateInternal(credential, clientClaims); + return builder.WithCertificate(credential.Certificate); case CredentialType.Secret: return builder.WithClientSecret(credential.ClientSecret); default: @@ -61,18 +58,6 @@ public static async Task WithClientCredent } } - private static ConfidentialClientApplicationBuilder WithCertificateInternal( - this ConfidentialClientApplicationBuilder builder, - CredentialDescription credentialDescription, - IDictionary? clientClaims = null) - { - if (clientClaims != null && clientClaims.Count > 0) - { - return builder.WithClientClaims(credentialDescription.Certificate, clientClaims); - } - return builder.WithCertificate(credentialDescription.Certificate); - } - internal /* for test */ async static Task LoadCredentialForMsalOrFailAsync( IEnumerable clientCredentials, ILogger logger, diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 0594a5b04..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Collections.Generic.IDictionary? clientClaims = null) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 66d05d304..bad72a6e2 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -215,17 +215,11 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn /// when supporting managed identities. /// /// Merged configuration options. - /// Optional client claims. /// Concatenated string of authority, cliend id and azure region - private static string GetApplicationKey(MergedOptions mergedOptions, IDictionary? clientClaims = null) + private static string GetApplicationKey(MergedOptions mergedOptions) { string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty()); - if (clientClaims != null) - { - credentialId += "-" + string.Join("-", clientClaims.Select(kvp => $"{kvp.Key}:{kvp.Value}")); - } - return DefaultTokenAcquirerFactoryImplementation.GetKey(mergedOptions.Authority, mergedOptions.ClientId, mergedOptions.AzureRegion) + credentialId; } @@ -450,6 +444,11 @@ public async Task GetAuthenticationResultForUserAsync( builder.WithCorrelationId(tokenAcquisitionOptions.CorrelationId.Value); } builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithSignedHttpRequestProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); @@ -565,6 +564,12 @@ public async Task GetAuthenticationResultForAppAsync( miBuilder.WithClaims(tokenAcquisitionOptions.Claims); } + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + miBuilder.WithExtraClientAssertionClaims(clientClaims); + } + return await miBuilder.ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) @@ -640,6 +645,13 @@ public async Task GetAuthenticationResultForAppAsync( } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } + if (!string.IsNullOrEmpty(tokenAcquisitionOptions.FmiPath)) { builder.WithFmiPath(tokenAcquisitionOptions.FmiPath); @@ -919,7 +931,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti TokenAcquisitionOptions? tokenAcquisitionOptions = null) // just for PoC will drive this through MergedOptions later { var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); - string key = GetApplicationKey(mergedOptions, clientClaims); + string key = GetApplicationKey(mergedOptions); // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680 // Fast path: check if already created @@ -937,7 +949,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return app; // Build and store the application - var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions, clientClaims); + var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions); // Recompute the key as BuildConfidentialClientApplicationAsync can cause it to change. key = GetApplicationKey(mergedOptions); @@ -953,7 +965,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti /// /// Creates an MSAL confidential client application. /// - private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions, IDictionary? clientClaims) + private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions) { mergedOptions.PrepareAuthorityInstanceForMsal(); @@ -1010,8 +1022,7 @@ await builder.WithClientCredentialsAsync( mergedOptions.ClientCredentials!, _logger, _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), - clientClaims); + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) { @@ -1197,6 +1208,11 @@ private void NotifyCertificateSelection( } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithSignedHttpRequestProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); @@ -1354,6 +1370,11 @@ private Task GetAuthenticationResultForWebAppWithAccountFr } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); From 401bdd3b3e8e2df292db6d4e0e984bb8033041df Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 3 Feb 2026 14:59:52 -0800 Subject: [PATCH 05/12] Update MSAL to 4.82 --- Directory.Build.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b4bf05d87..974c328ee 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -81,9 +81,9 @@ - 8.14.0 - 4.77.1 - 9.5.0 + 8.15.0 + 4.82.0 + 10.0.0 3.3.0 4.7.2 4.6.0 From ce7772f116c68e8c52d646b6865ead61f266902e Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 3 Feb 2026 15:14:14 -0800 Subject: [PATCH 06/12] Revert "Update MSAL to 4.82" This reverts commit 401bdd3b3e8e2df292db6d4e0e984bb8033041df. --- Directory.Build.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 974c328ee..b4bf05d87 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -81,9 +81,9 @@ - 8.15.0 - 4.82.0 - 10.0.0 + 8.14.0 + 4.77.1 + 9.5.0 3.3.0 4.7.2 4.6.0 From f1b1c6883f365e25ed98148f7811ba757800bdbf Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 3 Feb 2026 16:15:24 -0800 Subject: [PATCH 07/12] Updating tests and fixing errors --- Directory.Build.props | 2 +- .../TokenAcquisition.cs | 29 ++++++++++++------ .../TokenAcquisitionAuthorityTests.cs | 30 +++++++++++-------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b4bf05d87..e048e4a9b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -82,7 +82,7 @@ 8.14.0 - 4.77.1 + 4.82.0 9.5.0 3.3.0 4.7.2 diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index bad72a6e2..596aa847a 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -11,6 +11,7 @@ using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -154,7 +155,9 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn if (mergedOptions.ExtraQueryParameters != null) { +#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters((Dictionary)mergedOptions.ExtraQueryParameters); +#pragma warning restore CS0618 // Type or member is obsolete } if (!string.IsNullOrEmpty(authCodeRedemptionParameters.Tenant)) @@ -432,7 +435,9 @@ public async Task GetAuthenticationResultForUserAsync( var dict = MergeExtraQueryParameters(mergedOptions, tokenAcquisitionOptions); if (dict != null) { +#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters(dict); +#pragma warning restore CS0618 // Type or member is obsolete } if (tokenAcquisitionOptions.ExtraHeadersParameters != null) @@ -564,11 +569,12 @@ public async Task GetAuthenticationResultForAppAsync( miBuilder.WithClaims(tokenAcquisitionOptions.Claims); } - var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); - if (clientClaims != null) - { - miBuilder.WithExtraClientAssertionClaims(clientClaims); - } + //TODO: Should client assertion claims be supported for managed identity? + //var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + //if (clientClaims != null) + //{ + // miBuilder.WithExtraClientAssertionClaims(clientClaims); + //} return await miBuilder.ExecuteAsync().ConfigureAwait(false); } @@ -628,7 +634,9 @@ public async Task GetAuthenticationResultForAppAsync( if (dict != null) { +#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters(dict); +#pragma warning restore CS0618 // Type or member is obsolete } if (tokenAcquisitionOptions.ExtraHeadersParameters != null) { @@ -649,7 +657,7 @@ public async Task GetAuthenticationResultForAppAsync( var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); if (clientClaims != null) { - builder.WithExtraClientAssertionClaims(clientClaims); + builder.WithExtraClientAssertionClaims(JsonSerializer.Serialize(clientClaims)); } if (!string.IsNullOrEmpty(tokenAcquisitionOptions.FmiPath)) @@ -913,7 +921,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti ); } - private static IDictionary? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) + private static string? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) { IDictionary? clientClaims = null; if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && @@ -921,7 +929,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti { clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as IDictionary; } - return clientClaims; + return JsonSerializer.Serialize(clientClaims); } #pragma warning disable RS0051 // Add internal types and members to the declared API @@ -930,7 +938,6 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti MergedOptions mergedOptions, TokenAcquisitionOptions? tokenAcquisitionOptions = null) // just for PoC will drive this through MergedOptions later { - var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); string key = GetApplicationKey(mergedOptions); // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680 @@ -1196,7 +1203,9 @@ private void NotifyCertificateSelection( dict.Remove(subAssertionConstant); } +#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters(dict); +#pragma warning restore CS0618 // Type or member is obsolete } if (tokenAcquisitionOptions.ExtraHeadersParameters != null) { @@ -1358,7 +1367,9 @@ private Task GetAuthenticationResultForWebAppWithAccountFr if (dict != null) { +#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters(dict); +#pragma warning restore CS0618 // Type or member is obsolete } if (tokenAcquisitionOptions.ExtraHeadersParameters != null) { diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index c35c45081..9a9f18bbe 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -610,7 +610,7 @@ public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsy scope: "https://graph.microsoft.com/.default", authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, tenant: tenantId, - tokenAcquisitionOptions: null); + tokenAcquisitionOptions: tokenAcquisitionOptions); // Assert first network call produced client assertion with claims Assert.NotNull(result.AccessToken); @@ -625,7 +625,7 @@ public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsy scope: "https://graph.microsoft.com/.default", authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, tenant: tenantId, - tokenAcquisitionOptions: null); + tokenAcquisitionOptions: tokenAcquisitionOptions); Assert.NotNull(result2.AccessToken); Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); } @@ -682,13 +682,13 @@ public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); - var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); Assert.NotNull(first.AccessToken); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); var payloadJson = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); Assert.Contains("value_one", payloadJson, StringComparison.Ordinal); capturingHandler.ResetCapture(); - var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); Assert.NotNull(second.AccessToken); // Option A expectation: cached token => no new client_assertion sent Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); @@ -725,6 +725,11 @@ public async Task ClientClaims_ForceRefresh_NewAssertionAsync() { ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } }; + var forceOptions = new TokenAcquisitionOptions + { + ForceRefresh = true, + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); var services = new ServiceCollection(); @@ -743,12 +748,12 @@ public async Task ClientClaims_ForceRefresh_NewAssertionAsync() MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); - var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); Assert.NotNull(first.AccessToken); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); var firstAssertion = capturingHandler.CapturedClientAssertion; capturingHandler.ResetCapture(); - var forceOptions = new TokenAcquisitionOptions { ForceRefresh = true }; // Option B + // Option B var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, forceOptions); Assert.NotNull(second.AccessToken); // New network call expected (assertion recaptured) @@ -807,7 +812,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, initialOptions); - var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, null); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, initialOptions); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); var firstPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); Assert.Contains("v1", firstPayload, StringComparison.Ordinal); @@ -815,6 +820,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var newClaims = new Dictionary { { "c1", "v2" }, { "c2", "vNew" } }; var newOptions = new TokenAcquisitionOptions { + ForceRefresh = true, ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", newClaims } } }; // Call GetOrBuild again with new claims @@ -822,14 +828,12 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() // Same instance expected Assert.Same(app2, await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, null)); capturingHandler.ResetCapture(); - var forceRefresh = new TokenAcquisitionOptions { ForceRefresh = true }; // network call - var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, forceRefresh); + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, newOptions); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); var secondPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); // Validate old value still present and new ones absent - Assert.Contains("v1", secondPayload, StringComparison.Ordinal); - Assert.DoesNotContain("v2", secondPayload, StringComparison.Ordinal); - Assert.DoesNotContain("vNew", secondPayload, StringComparison.Ordinal); + Assert.Contains("v2", secondPayload, StringComparison.Ordinal); + Assert.DoesNotContain("v1", secondPayload, StringComparison.Ordinal); } private static string DecodeJwtPayload(string jwt) @@ -858,7 +862,7 @@ private class CapturingMsalHttpClientFactory : IMsalHttpClientFactory private class CapturingHandler : HttpMessageHandler { private readonly string _authorityBase; - public string? CapturedClientAssertion { get; private set; } + public string? CapturedClientAssertion {get; private set; } public CapturingHandler(string authorityBase) => _authorityBase = authorityBase.TrimEnd('/'); public void ResetCapture() => CapturedClientAssertion = null; protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) From 031f73b4ba01606de951d9a5bcb48e9e01ed50f0 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 3 Feb 2026 22:17:06 -0800 Subject: [PATCH 08/12] Fixing build --- .../TokenAcquisition.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 596aa847a..6eec44f0f 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -657,7 +657,7 @@ public async Task GetAuthenticationResultForAppAsync( var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); if (clientClaims != null) { - builder.WithExtraClientAssertionClaims(JsonSerializer.Serialize(clientClaims)); + builder.WithExtraClientAssertionClaims(clientClaims); } if (!string.IsNullOrEmpty(tokenAcquisitionOptions.FmiPath)) @@ -921,6 +921,10 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti ); } +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] + [RequiresDynamicCode("Calls JsonSerializer.Serialize")] +#endif private static string? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) { IDictionary? clientClaims = null; From ae9841f870dc946e604774ae40b3af51d055cc83 Mon Sep 17 00:00:00 2001 From: trwalke Date: Tue, 3 Feb 2026 22:48:14 -0800 Subject: [PATCH 09/12] Fix build --- .../TokenAcquisition.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 06325210a..bb892018c 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -352,6 +352,10 @@ public async Task GetAuthenticationResultForUserAsync( } } +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] + [RequiresDynamicCode("Calls JsonSerializer.Serialize")] +#endif // This method mutate the user claims to include claims uid and utid to perform the silent flow for subsequent calls. private async Task TryGetAuthenticationResultForConfidentialClientUsingRopcAsync( IConfidentialClientApplication application, @@ -541,6 +545,10 @@ private void LogAuthResult(AuthenticationResult? authenticationResult) /// for multi tenant apps or daemons. /// Options passed-in to create the token acquisition object which calls into MSAL .NET. /// An authentication result for the app itself, based on its scopes. +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] + [RequiresDynamicCode("Calls JsonSerializer.Serialize")] +#endif public async Task GetAuthenticationResultForAppAsync( string scope, string? authenticationScheme = null, @@ -1179,6 +1187,10 @@ private void NotifyCertificateSelection( } } +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] + [RequiresDynamicCode("Calls JsonSerializer.Serialize")] +#endif private async ValueTask GetAuthenticationResultForWebApiToCallDownstreamApiAsync( IConfidentialClientApplication application, string? tenantId, @@ -1438,6 +1450,10 @@ private async ValueTask GetAuthenticationResultForWebAppWi /// Merged options. /// Azure AD B2C user flow. /// Options passed-in to create the token acquisition object which calls into MSAL .NET. +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] + [RequiresDynamicCode("Calls JsonSerializer.Serialize")] +#endif private Task GetAuthenticationResultForWebAppWithAccountFromCacheAsync( IConfidentialClientApplication application, IAccount? account, From 1283deeaca14e9b4304a47dfbe4fec5872292653 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 4 Feb 2026 01:23:14 -0800 Subject: [PATCH 10/12] Refactoring to use string --- .../TokenAcquisition.cs | 26 +++---------------- .../TokenAcquisitionAuthorityTests.cs | 25 +++++++++--------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index bb892018c..432621c5b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -352,10 +352,6 @@ public async Task GetAuthenticationResultForUserAsync( } } -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] - [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif // This method mutate the user claims to include claims uid and utid to perform the silent flow for subsequent calls. private async Task TryGetAuthenticationResultForConfidentialClientUsingRopcAsync( IConfidentialClientApplication application, @@ -545,10 +541,6 @@ private void LogAuthResult(AuthenticationResult? authenticationResult) /// for multi tenant apps or daemons. /// Options passed-in to create the token acquisition object which calls into MSAL .NET. /// An authentication result for the app itself, based on its scopes. -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] - [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif public async Task GetAuthenticationResultForAppAsync( string scope, string? authenticationScheme = null, @@ -963,19 +955,15 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti #endif } -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] - [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif private static string? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) { - IDictionary? clientClaims = null; + string? clientClaims = null; if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] is not null) { - clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as IDictionary; + clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as string; } - return JsonSerializer.Serialize(clientClaims); + return clientClaims; } #pragma warning disable RS0051 // Add internal types and members to the declared API @@ -1187,10 +1175,6 @@ private void NotifyCertificateSelection( } } -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] - [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif private async ValueTask GetAuthenticationResultForWebApiToCallDownstreamApiAsync( IConfidentialClientApplication application, string? tenantId, @@ -1450,10 +1434,6 @@ private async ValueTask GetAuthenticationResultForWebAppWi /// Merged options. /// Azure AD B2C user flow. /// Options passed-in to create the token acquisition object which calls into MSAL .NET. -#if NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] - [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif private Task GetAuthenticationResultForWebAppWithAccountFromCacheAsync( IConfidentialClientApplication application, IAccount? account, diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index 37fc40bd0..eb205f316 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -578,7 +579,7 @@ public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsy { ExtraParameters = new Dictionary { - { "IDWEB_CLIENT_CLAIMS", customClaims } + { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; @@ -605,7 +606,7 @@ public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsy MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); // Act first token acquisition (network call expected) - await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); var result = await _tokenAcquisition.GetAuthenticationResultForAppAsync( scope: "https://graph.microsoft.com/.default", authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, @@ -662,7 +663,7 @@ public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() }; var tokenAcquisitionOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -681,7 +682,7 @@ public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); - await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); Assert.NotNull(first.AccessToken); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); @@ -723,12 +724,12 @@ public async Task ClientClaims_ForceRefresh_NewAssertionAsync() var customClaims = new Dictionary { { "claimX", "claimXValue" } }; var tokenAcquisitionOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var forceOptions = new TokenAcquisitionOptions { ForceRefresh = true, - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", customClaims } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -747,7 +748,7 @@ public async Task ClientClaims_ForceRefresh_NewAssertionAsync() var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); - await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, tokenAcquisitionOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); Assert.NotNull(first.AccessToken); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); @@ -792,7 +793,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var initialClaims = new Dictionary { { "c1", "v1" } }; var initialOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", initialClaims } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(initialClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -811,7 +812,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); - await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, initialOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, initialOptions); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); var firstPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); @@ -821,12 +822,12 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var newOptions = new TokenAcquisitionOptions { ForceRefresh = true, - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", newClaims } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(newClaims) } } }; // Call GetOrBuild again with new claims - var app2 = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, newOptions); + var app2 = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); // Same instance expected - Assert.Same(app2, await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, null)); + Assert.Same(app2, await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false)); capturingHandler.ResetCapture(); var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, newOptions); Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); From f429c8f294de39508ddd359c983565dfb3a5d96b Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 4 Feb 2026 01:38:19 -0800 Subject: [PATCH 11/12] Fixing null ref --- src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 432621c5b..620a6fc30 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -959,7 +959,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti { string? clientClaims = null; if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && - tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] is not null) + tokenAcquisitionOptions.ExtraParameters.ContainsKey("IDWEB_CLIENT_CLAIMS")) { clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as string; } From 876c14d6c04f9c1b62941690cd637af12c7d0de9 Mon Sep 17 00:00:00 2001 From: trwalke Date: Wed, 4 Feb 2026 11:40:16 -0800 Subject: [PATCH 12/12] Updated claims key --- .../TokenAcquisition.cs | 6 ++---- .../TokenAcquisitionAuthorityTests.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 620a6fc30..29a5ffe7d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -157,9 +157,7 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn if (mergedOptions.ExtraQueryParameters != null) { -#pragma warning disable CS0618 // Type or member is obsolete builder.WithExtraQueryParameters(MergeExtraQueryParameters(mergedOptions, null)); -#pragma warning restore CS0618 // Type or member is obsolete } if (!string.IsNullOrEmpty(authCodeRedemptionParameters.Tenant)) @@ -959,9 +957,9 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti { string? clientClaims = null; if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && - tokenAcquisitionOptions.ExtraParameters.ContainsKey("IDWEB_CLIENT_CLAIMS")) + tokenAcquisitionOptions.ExtraParameters.ContainsKey("IDWEB_CLIENT_ASSERTION_CLAIMS")) { - clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_CLAIMS"] as string; + clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_ASSERTION_CLAIMS"] as string; } return clientClaims; } diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index eb205f316..c93a0b8ae 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -579,7 +579,7 @@ public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsy { ExtraParameters = new Dictionary { - { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } + { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; @@ -663,7 +663,7 @@ public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() }; var tokenAcquisitionOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -724,12 +724,12 @@ public async Task ClientClaims_ForceRefresh_NewAssertionAsync() var customClaims = new Dictionary { { "claimX", "claimXValue" } }; var tokenAcquisitionOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var forceOptions = new TokenAcquisitionOptions { ForceRefresh = true, - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(customClaims) } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -793,7 +793,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var initialClaims = new Dictionary { { "c1", "v1" } }; var initialOptions = new TokenAcquisitionOptions { - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(initialClaims) } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(initialClaims) } } }; var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); @@ -822,7 +822,7 @@ public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() var newOptions = new TokenAcquisitionOptions { ForceRefresh = true, - ExtraParameters = new Dictionary { { "IDWEB_CLIENT_CLAIMS", JsonSerializer.Serialize(newClaims) } } + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(newClaims) } } }; // Call GetOrBuild again with new claims var app2 = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false);