diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln
index 35d5a8626..5c03b1714 100644
--- a/Microsoft.Identity.Web.sln
+++ b/Microsoft.Identity.Web.sln
@@ -158,6 +158,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorApp", "tests\DevApps\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "blazor", "blazor", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OidcIdpSignedAssertionProviderTests", "tests\E2E Tests\OidcIdPSignedAssertionProviderTests\OidcIdpSignedAssertionProviderTests.csproj", "{E927D215-A96C-626C-9A1A-CF99876FE7B4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.OidcFIC", "src\Microsoft.Identity.Web.OidcFIC\Microsoft.Identity.Web.OidcFIC.csproj", "{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -369,6 +373,14 @@ Global
{4D67BE6A-79CD-42E7-8748-C909FCC394DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D67BE6A-79CD-42E7-8748-C909FCC394DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D67BE6A-79CD-42E7-8748-C909FCC394DF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E927D215-A96C-626C-9A1A-CF99876FE7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E927D215-A96C-626C-9A1A-CF99876FE7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E927D215-A96C-626C-9A1A-CF99876FE7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E927D215-A96C-626C-9A1A-CF99876FE7B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -440,6 +452,8 @@ Global
{A390650C-BCE1-4CB3-8C97-9EF9CFF5B7C5} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
{4D67BE6A-79CD-42E7-8748-C909FCC394DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {7786D2DD-9EE4-42E1-B587-740A2E15C41D}
+ {E927D215-A96C-626C-9A1A-CF99876FE7B4} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187}
diff --git a/src/Microsoft.Identity.Web.OidcFIC/Microsoft.Identity.Web.OidcFIC.csproj b/src/Microsoft.Identity.Web.OidcFIC/Microsoft.Identity.Web.OidcFIC.csproj
new file mode 100644
index 000000000..b29fab2fd
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/Microsoft.Identity.Web.OidcFIC.csproj
@@ -0,0 +1,32 @@
+
+
+
+ Microsoft Identity Web Token Cross Cloud Federation Identity Credential (FIC) support
+ Microsoft Identity Web Cross Cloud FIC
+ Implementation for a Cloud Federation Identity Credential (FIC) credential provider.
+ {8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A}
+ README.md
+
+
+ false
+
+
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Identity.Web.OidcFIC/OidcFicSignedAssertionProviderExtensions.cs b/src/Microsoft.Identity.Web.OidcFIC/OidcFicSignedAssertionProviderExtensions.cs
new file mode 100644
index 000000000..6f3d118f6
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/OidcFicSignedAssertionProviderExtensions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.OidcFic;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension class to add OIDC FIC signed assertion provider to the service collection
+ ///
+ ///
+ public static class OidcFicSignedAssertionProviderExtensions
+ {
+ ///
+ /// Adds OIDC FIC signed assertion provider to the service collection
+ ///
+ /// service collection
+ /// the service collection for chaining.
+ public static IServiceCollection AddOidcFic(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ return services;
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs
new file mode 100644
index 000000000..ccfcd74c9
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionLoader.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Identity.Abstractions;
+
+namespace Microsoft.Identity.Web.OidcFic
+{
+ internal class OidcIdpSignedAssertionLoader : ICustomSignedAssertionProvider
+ {
+ private readonly ILogger _logger;
+ private readonly IOptionsMonitor _options;
+ private readonly IConfiguration _configuration;
+ private readonly ITokenAcquirerFactory _tokenAcquirerFactory;
+
+ public OidcIdpSignedAssertionLoader(ILogger logger,
+ IOptionsMonitor options,
+ IConfiguration configuration,
+ ITokenAcquirerFactory tokenAcquirerFactory)
+ {
+ _logger = logger;
+ _options = options;
+ _configuration = configuration;
+ _tokenAcquirerFactory = tokenAcquirerFactory;
+ }
+
+ public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion;
+
+ public string Name => "OidcIdpSignedAssertion";
+
+
+ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null)
+ {
+ OidcIdpSignedAssertionProvider? signedAssertion = credentialDescription.CachedValue as OidcIdpSignedAssertionProvider;
+ if (credentialDescription.CachedValue == null)
+ {
+ if (credentialDescription.CustomSignedAssertionProviderData == null)
+ {
+ if (_logger != null)
+ {
+ _logger.LogError(42, "CustomSignedAssertionProviderData is null");
+ }
+ throw new InvalidOperationException("CustomSignedAssertionProviderData is null");
+ }
+
+ string? sectionName = credentialDescription.CustomSignedAssertionProviderData["ConfigurationSection"] as string;
+ if (sectionName == null)
+ {
+ if (_logger != null)
+ {
+ _logger.LogError(42, "ConfigurationSection is null");
+ }
+ throw new InvalidOperationException("ConfigurationSection is null");
+ }
+
+ MicrosoftIdentityApplicationOptions microsoftIdentityApplicationOptions = _options.Get(sectionName);
+
+ if (string.IsNullOrEmpty(microsoftIdentityApplicationOptions.Instance) && microsoftIdentityApplicationOptions.Authority == "//v2.0")
+ {
+ _configuration.GetSection(sectionName).Bind(microsoftIdentityApplicationOptions);
+ }
+
+ signedAssertion = new OidcIdpSignedAssertionProvider(_tokenAcquirerFactory, microsoftIdentityApplicationOptions, credentialDescription.TokenExchangeUrl);
+ }
+
+ try
+ {
+ // Try to get a signed assertion, and if it fails, move to the next credentials
+ _ = await signedAssertion!.GetSignedAssertionAsync(null);
+ credentialDescription.CachedValue = signedAssertion;
+ }
+ catch (Exception ex)
+ {
+ if (_logger != null)
+ {
+ _logger.LogError(42, "Failed to get signed assertion from {ProviderName}. exception occurred: {Message}. Setting skip to true.", credentialDescription.CustomSignedAssertionProviderName, ex.Message);
+ }
+ credentialDescription.Skip = true;
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs
new file mode 100644
index 000000000..69d2d4848
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Web;
+
+namespace Microsoft.Identity.Web.OidcFic
+{
+ internal class OidcIdpSignedAssertionProvider : ClientAssertionProviderBase
+ {
+ private ITokenAcquirer? _tokenAcquirer = null;
+ private readonly ITokenAcquirerFactory _tokenAcquirerFactory;
+ private readonly MicrosoftIdentityApplicationOptions _options;
+ private readonly string? _tokenExchangeUrl;
+
+ public OidcIdpSignedAssertionProvider(ITokenAcquirerFactory tokenAcquirerFactory, MicrosoftIdentityApplicationOptions options, string? tokenExchangeUrl)
+ {
+ _tokenAcquirerFactory = tokenAcquirerFactory;
+ _options = options;
+ _tokenExchangeUrl = tokenExchangeUrl;
+ }
+
+ protected override async Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions)
+ {
+ _tokenAcquirer ??= _tokenAcquirerFactory.GetTokenAcquirer(_options);
+
+ string tokenExchangeUrl = _tokenExchangeUrl ?? "api://AzureADTokenExchange";
+
+ AcquireTokenResult result = await _tokenAcquirer.GetTokenForAppAsync(tokenExchangeUrl + "/.default");
+ ClientAssertion clientAssertion;
+ if (result != null)
+ {
+ clientAssertion = new ClientAssertion(result.AccessToken!, result.ExpiresOn);
+ }
+ else
+ {
+ clientAssertion = null!;
+ }
+ return clientAssertion;
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net6.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net7.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt
new file mode 100644
index 000000000..d4e89a673
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.CredentialSource.get -> Microsoft.Identity.Abstractions.CredentialSource
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.LoadIfNeededAsync(Microsoft.Identity.Abstractions.CredentialDescription! credentialDescription, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? parameters = null) -> System.Threading.Tasks.Task!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Name.get -> string!
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.OidcIdpSignedAssertionLoader(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory) -> void
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider
+Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.OidcIdpSignedAssertionProvider(Microsoft.Identity.Abstractions.ITokenAcquirerFactory! tokenAcquirerFactory, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options, string? tokenExchangeUrl) -> void
+override Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.GetClientAssertionAsync(Microsoft.Identity.Client.AssertionRequestOptions? assertionRequestOptions) -> System.Threading.Tasks.Task!
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
new file mode 100644
index 000000000..7dc5c5811
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..a082b4e3a
--- /dev/null
+++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions
+static Microsoft.Extensions.DependencyInjection.OidcFicSignedAssertionProviderExtensions.AddOidcFic(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
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 e69de29bb..e2c502930 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
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 e69de29bb..e2c502930 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/PublicAPI.Unshipped.txt
index e69de29bb..e2c502930 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net6.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/PublicAPI.Unshipped.txt
index e69de29bb..e2c502930 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net7.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
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 e69de29bb..e2c502930 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
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
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 e69de29bb..e2c502930 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
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
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 e69de29bb..e2c502930 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
@@ -0,0 +1,3 @@
+#nullable enable
+Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting
+static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs
index 555f60f6c..98b69df6c 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs
@@ -185,7 +185,14 @@ protected virtual void PreBuild()
/// Resets the default instance. Useful for tests as token acquirer factory is a singleton
/// in most configurations (except ASP.NET Core)
///
- internal /* for unit tests */ static void ResetDefaultInstance() { defaultInstance = null; }
+ internal /* for unit tests */ static void ResetDefaultInstance()
+ {
+ if (defaultInstance?.ServiceProvider != null)
+ {
+ (defaultInstance.ServiceProvider as IDisposable)?.Dispose();
+ }
+ defaultInstance = null;
+ }
// Move to a derived class?
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactoryTesting.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactoryTesting.cs
new file mode 100644
index 000000000..0bb9a6294
--- /dev/null
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactoryTesting.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Identity.Web.TestOnly
+{
+ ///
+ /// Class that should only be used in tests, not product code, used
+ /// to reset the default instance of the token acquirer factory.
+ ///
+ public static class TokenAcquirerFactoryTesting
+ {
+ ///
+ /// Resets the default instance of the token acquirer factory.
+ /// Use in tests, but not in production code.
+ ///
+ public static void ResetTokenAcquirerFactoryInTest()
+ {
+ TokenAcquirerFactory.ResetDefaultInstance();
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
index 697c48e6d..7256363d4 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
@@ -45,12 +45,13 @@ class OAuthConstants
#endif
protected readonly IMsalTokenCacheProvider _tokenCacheProvider;
- private SemaphoreSlim _applicationSync = new (1, 1);
-
///
- /// Please call GetOrBuildConfidentialClientApplication instead of accessing _applicationsByAuthorityClientId directly.
+ /// Important: call GetOrBuildConfidentialClientApplication instead of accessing _applicationsByAuthorityClientId directly.
+ /// Write access to this dictionary is synchronized.
///
private readonly ConcurrentDictionary _applicationsByAuthorityClientId = new();
+ private readonly ConcurrentDictionary _appSemaphores = new();
+
private bool _retryClientCertificate;
protected readonly IMsalHttpClientFactory _httpClientFactory;
protected readonly ILogger _logger;
@@ -517,7 +518,7 @@ public async Task GetAuthenticationResultForAppAsync(
.AcquireTokenForClient(new[] { scope }.Except(_scopesRequestedByMsal))
.WithSendX5C(mergedOptions.SendX5C);
- if (addInOptions!=null)
+ if (addInOptions != null)
{
addInOptions.InvokeOnBeforeTokenAcquisitionForApp(builder, tokenAcquisitionOptions);
}
@@ -745,36 +746,45 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti
|| exMsal.Message.Contains(Constants.CertificateHasBeenRevoked, StringComparison.OrdinalIgnoreCase)
|| exMsal.Message.Contains(Constants.CertificateIsOutsideValidityWindow, StringComparison.OrdinalIgnoreCase));
#else
- (exMsal.Message.Contains(Constants.InvalidKeyError)
- || exMsal.Message.Contains(Constants.SignedAssertionInvalidTimeRange)
+ (exMsal.Message.Contains(Constants.InvalidKeyError)
+ || exMsal.Message.Contains(Constants.SignedAssertionInvalidTimeRange)
|| exMsal.Message.Contains(Constants.CertificateHasBeenRevoked)
|| exMsal.Message.Contains(Constants.CertificateIsOutsideValidityWindow));
#endif
}
-
+
+
internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync(
- MergedOptions mergedOptions)
+ MergedOptions mergedOptions)
{
- if (!_applicationsByAuthorityClientId.TryGetValue(GetApplicationKey(mergedOptions), out IConfidentialClientApplication? application) || application == null)
+ // Use all credentials to compute a credential chain ID. Each individual ID should be unique.
+ string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty());
+ string key = GetApplicationKey(mergedOptions) + credentialId;
+
+ // GetOrAddAsync based on https://github.com/dotnet/runtime/issues/83636#issuecomment-1474998680
+ // Fast path: check if already created
+ if (_applicationsByAuthorityClientId.TryGetValue(key, out var existingApp) && existingApp != null)
+ return existingApp;
+
+ // Get or create a semaphore for this specific key
+ var semaphore = _appSemaphores.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
+
+ await semaphore.WaitAsync();
+ try
{
- await _applicationSync.WaitAsync();
-
- try
- {
- if (!_applicationsByAuthorityClientId.TryGetValue(GetApplicationKey(mergedOptions), out application) ||
- application == null)
- {
- application = await BuildConfidentialClientApplicationAsync(mergedOptions);
- _applicationsByAuthorityClientId[GetApplicationKey(mergedOptions)] = application;
- }
- }
- finally
- {
- _applicationSync.Release();
- }
+ // Double-check after acquiring the lock
+ if (_applicationsByAuthorityClientId.TryGetValue(key, out var app) && app != null)
+ return app;
+
+ // Build and store the application
+ var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions);
+ _applicationsByAuthorityClientId[key] = newApp;
+ return newApp;
+ }
+ finally
+ {
+ semaphore.Release();
}
-
- return application;
}
///
diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config
index 2d99d4971..e9fe1092b 100644
--- a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config
+++ b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config
@@ -58,11 +58,11 @@
-
+
-
+
@@ -74,7 +74,7 @@
-
+
@@ -82,23 +82,23 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config
index 3c287d045..d24ccdce4 100644
--- a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config
+++ b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config
@@ -59,11 +59,11 @@
-
+
-
+
@@ -75,7 +75,7 @@
-
+
@@ -83,23 +83,23 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs
new file mode 100644
index 000000000..6f587feea
--- /dev/null
+++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web;
+using Microsoft.Identity.Web.Test.Common;
+using Microsoft.Identity.Web.Test.Common.Mocks;
+using Microsoft.Identity.Web.TestOnly;
+
+
+namespace CustomSignedAssertionProviderTests
+{
+ [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)]
+ public class NonParallelCollection : ICollectionFixture
+ {
+ // This class has no code, and is never created
+ }
+
+ public class NonParallelFixture
+ {
+ }
+
+ [Collection("Non-Parallel Collection")]
+ public class OidCIdPSignedAssertionProviderExtensibilityTests
+ {
+ [OnlyOnAzureDevopsFact]
+ public async Task CrossCloudFicIntegrationTest()
+ {
+ // Arrange
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
+ TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
+ tokenAcquirerFactory.Services.AddOidcFic();
+
+ // this is how the authentication options can be configured in code rather than
+ // in the appsettings file, though using the appsettings file is recommended
+ /*
+ tokenAcquirerFactory.Services.Configure(options =>
+ {
+ options.Instance = "https://login.microsoftonline.com/";
+ options.TenantId = "msidlab4.onmicrosoft.com";
+ options.ClientId = "5e71875b-ae52-4a3c-8b82-f6fdc8e1dbe1";
+ options.ClientCredentials = [ new CredentialDescription() {
+ SourceType = CredentialSource.CustomSignedAssertion,
+ CustomSignedAssertionProviderName = "MyCustomExtension"
+ }];
+ });
+ */
+ IServiceProvider serviceProvider = tokenAcquirerFactory.Build();
+ IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService();
+
+ // Act
+ string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.StartsWith("Bearer", result, StringComparison.Ordinal);
+ }
+
+ //[Fact(Skip ="Does not run if run with the E2E test")]
+ [Fact]
+ public async Task CrossCloudFicUnitTest()
+ {
+ // Arrange
+ using (MockHttpClientFactory httpFactoryForTest = new MockHttpClientFactory())
+ {
+ var credentialRequestHttpHandler = httpFactoryForTest.AddMockHandler(
+ MockHttpCreator.CreateClientCredentialTokenHandler());
+ var tokenRequestHttpHandler = httpFactoryForTest.AddMockHandler(
+ MockHttpCreator.CreateClientCredentialTokenHandler());
+
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
+ TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
+ tokenAcquirerFactory.Services.AddOidcFic();
+ tokenAcquirerFactory.Services.AddSingleton(httpFactoryForTest);
+
+ tokenAcquirerFactory.Services.Configure("AzureAd2",
+ options =>
+ {
+ options.Instance = "https://login.microsoftonline.us/";
+ options.TenantId = "t1";
+ options.ClientId = "c1";
+ options.ClientCredentials = [ new CredentialDescription() {
+ SourceType = CredentialSource.ClientSecret,
+ ClientSecret = TestConstants.ClientSecret
+ }];
+ });
+
+ tokenAcquirerFactory.Services.Configure(options =>
+ {
+ options.Instance = "https://login.microsoftonline.com/";
+ options.TenantId = "t2";
+ options.ClientId = "c2";
+ options.ExtraQueryParameters = null;
+ options.ClientCredentials = [ new CredentialDescription() {
+ SourceType = CredentialSource.CustomSignedAssertion,
+ CustomSignedAssertionProviderName = "OidcIdpSignedAssertion",
+ CustomSignedAssertionProviderData = new Dictionary{{
+ "ConfigurationSection", "AzureAd2"
+ }}
+ }];
+ });
+
+ IServiceProvider serviceProvider = tokenAcquirerFactory.Build();
+ IAuthorizationHeaderProvider authorizationHeaderProvider =
+ serviceProvider.GetRequiredService();
+
+ // Act
+ var result = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(TestConstants.s_scopeForApp);
+
+ // Assert
+ Assert.Equal("api://AzureADTokenExchange/.default", credentialRequestHttpHandler.ActualRequestPostData["scope"]);
+ Assert.Equal(TestConstants.s_scopeForApp, tokenRequestHttpHandler.ActualRequestPostData["scope"]);
+ Assert.Equal("c1", credentialRequestHttpHandler.ActualRequestPostData["client_id"]);
+ Assert.Equal("https://login.microsoftonline.us/t1/oauth2/v2.0/token", credentialRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri);
+ Assert.Equal("c2", tokenRequestHttpHandler.ActualRequestPostData["client_id"]);
+ Assert.Equal("https://login.microsoftonline.com/t2/oauth2/v2.0/token", tokenRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri);
+
+ string? accessTokenFromRequest1;
+ using (JsonDocument document = JsonDocument.Parse(credentialRequestHttpHandler.ResponseString))
+ {
+ accessTokenFromRequest1 = document.RootElement.GetProperty("access_token").GetString();
+ }
+
+ // the jwt credential from request1 is used as credential on request2
+ Assert.Equal(
+ tokenRequestHttpHandler.ActualRequestPostData["client_assertion"],
+ accessTokenFromRequest1);
+ }
+ }
+ }
+}
diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidcIdpSignedAssertionProviderTests.csproj b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidcIdpSignedAssertionProviderTests.csproj
new file mode 100644
index 000000000..f00838baa
--- /dev/null
+++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidcIdpSignedAssertionProviderTests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net9.0
+ ../../../build/MSAL.snk
+ enable
+ false
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json
new file mode 100644
index 000000000..07aeb14f0
--- /dev/null
+++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://raw.githubusercontent.com/AzureAD/microsoft-identity-web/refs/heads/master/JsonSchemas/microsoft-identity-web.json",
+ "AzureAd": {
+ "Instance": "https://login.microsoftonline.com/",
+ "TenantId": "msidlab4.onmicrosoft.com",
+ "ExtraQueryParameters": { "dc": "ESTS-PUB-WEULR1-AZ1-FD000-TEST1" },
+ "ClientId": "5e71875b-ae52-4a3c-8b82-f6fdc8e1dbe1", // this app is configured to trust credentials (tokens) from f6b698c0-140c-448f-8155-4aa9bf77ceba
+ "ClientCredentials": [
+ {
+ "SourceType": "CustomSignedAssertion",
+ "CustomSignedAssertionProviderName": "OidcIdpSignedAssertion",
+ "CustomSignedAssertionProviderData": {
+ "ConfigurationSection": "AzureAd2"
+ }
+ }
+ ]
+ },
+ "AzureAd2": {
+ "Instance": "https://login.microsoftonline.us/",
+ "TenantId": "45ff0c17-f8b5-489b-b7fd-2fedebbec0c4",
+ "ClientId": "f13080ee-01fe-48c1-8e9f-f0dd6f69ac7b",
+ "ExtraQueryParameters": { "dc": "ESTS-PUB-WEULR1-AZ1-FD000-TEST1" },
+ "SendX5C": true,
+ "ClientCredentials": [
+ {
+ "SourceType": "StoreWithDistinguishedName",
+ "CertificateStorePath": "CurrentUser/My",
+ "CertificateDistinguishedName": "CN=LabAuth.MSIDLab.com"
+ }
+ ]
+ }
+}
diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/xunit.runner.json b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/xunit.runner.json
new file mode 100644
index 000000000..054571c26
--- /dev/null
+++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "parallelizeTestCollections": false
+}
diff --git a/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs b/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs
index 655c05331..fda3d551d 100644
--- a/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs
+++ b/tests/E2E Tests/TokenAcquirerTests/CertificateRotationTest.cs
@@ -14,6 +14,7 @@
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Experimental;
+using Microsoft.Identity.Web.Test.Common;
using Xunit;
namespace TokenAcquirerTests
@@ -222,7 +223,8 @@ await _graphServiceClient.ServicePrincipals[$"{_servicePrincipal!.Id}"]
}
}
}
- }
+ },
+ ServiceManagementReference = "20504242-2c9d-4a5f-aac8-684e401e1119",
};
Application createdApp = (await _graphServiceClient.Applications
.PostAsync(application))!;
diff --git a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs
index 61820c9ce..2d116d10b 100644
--- a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs
+++ b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs
@@ -15,6 +15,7 @@
using Microsoft.Identity.Lab.Api;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Test.Common;
+using Microsoft.Identity.Web.TestOnly;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Microsoft.IdentityModel.Tokens;
using Xunit;
@@ -40,14 +41,10 @@ public class TokenAcquirer
"AzureADIdentityDivisionTestAgentCert")
};
- public TokenAcquirer()
- {
- TokenAcquirerFactory.ResetDefaultInstance(); // Test only
- }
-
[Fact]
public void TokenAcquirerFactoryDoesNotUseAspNetCoreHost()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
var serviceProvider = tokenAcquirerFactory.Build();
var service = serviceProvider.GetService();
@@ -68,6 +65,7 @@ public void DefaultTokenAcquirer_GetKeyHandlesNulls()
[Fact]
public void AcquireToken_WithMultipleRegions()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
_ = tokenAcquirerFactory.Build();
@@ -96,6 +94,7 @@ public void AcquireToken_WithMultipleRegions()
[Fact]
public async Task AcquireToken_ROPC_CCAasync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
_ = tokenAcquirerFactory.Build();
@@ -125,6 +124,7 @@ public async Task AcquireToken_ROPC_CCAasync()
[Fact]
public async Task AcquireToken_ROPC_CCA_WithForceRefresh_async()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
_ = tokenAcquirerFactory.Build();
@@ -156,6 +156,7 @@ public async Task AcquireToken_ROPC_CCA_WithForceRefresh_async()
[Fact]
public void AcquireToken_SafeFromMultipleThreads()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
_ = tokenAcquirerFactory.Build();
@@ -197,6 +198,7 @@ public void AcquireToken_SafeFromMultipleThreads()
public async Task AcquireToken_WithMicrosoftIdentityOptions_ClientCredentialsAsync(/*bool withClientCredentials*/)
{
bool withClientCredentials = false; //add as param above
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
@@ -222,6 +224,7 @@ public async Task AcquireToken_WithMicrosoftIdentityOptions_ClientCredentialsAsy
//[Fact]
public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_ClientCredentialsAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
@@ -240,6 +243,7 @@ public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_ClientCre
//[Fact]
public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_ClientCredentialsCiamAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
@@ -257,6 +261,7 @@ public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_ClientCre
// [Fact]
public async Task AcquireToken_WithFactoryAndMicrosoftIdentityApplicationOptions_ClientCredentialsAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
tokenAcquirerFactory.Services.AddInMemoryTokenCaches();
tokenAcquirerFactory.Build();
@@ -277,6 +282,7 @@ public async Task AcquireToken_WithFactoryAndMicrosoftIdentityApplicationOptions
// [Fact]
public async Task AcquireToken_WithFactoryAndAuthorityClientIdCert_ClientCredentialsAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
tokenAcquirerFactory.Services.AddInMemoryTokenCaches();
tokenAcquirerFactory.Build();
@@ -294,6 +300,7 @@ public async Task AcquireToken_WithFactoryAndAuthorityClientIdCert_ClientCredent
//[Fact]
public async Task LoadCredentialsIfNeededAsync_MultipleThreads_WaitsForSemaphoreAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
@@ -334,6 +341,7 @@ public async Task LoadCredentialsIfNeededAsync_MultipleThreads_WaitsForSemaphore
//[Fact]
public async Task AcquireTokenWithPop_ClientCredentialsAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
@@ -363,6 +371,7 @@ public async Task AcquireTokenWithPop_ClientCredentialsAsync()
//[Fact]
public async Task AcquireTokenWithMs10AtPop_ClientCredentialsAsync()
{
+ TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest();
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
IServiceCollection services = tokenAcquirerFactory.Services;
diff --git a/tests/E2E Tests/TokenAcquirerTests/IgnoreOnAzureDevOpsFactAttribute.cs b/tests/Microsoft.Identity.Web.Test.Common/IgnoreOnAzureDevOpsFactAttribute.cs
similarity index 94%
rename from tests/E2E Tests/TokenAcquirerTests/IgnoreOnAzureDevOpsFactAttribute.cs
rename to tests/Microsoft.Identity.Web.Test.Common/IgnoreOnAzureDevOpsFactAttribute.cs
index bc2fcffde..f084c808f 100644
--- a/tests/E2E Tests/TokenAcquirerTests/IgnoreOnAzureDevOpsFactAttribute.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/IgnoreOnAzureDevOpsFactAttribute.cs
@@ -4,9 +4,8 @@
using System;
using Xunit;
-namespace TokenAcquirerTests
+namespace Microsoft.Identity.Web.Test.Common
{
-
public sealed class IgnoreOnAzureDevopsFactAttribute : FactAttribute
{
public IgnoreOnAzureDevopsFactAttribute()
diff --git a/tests/Microsoft.Identity.Web.Test.Common/Microsoft.Identity.Web.Test.Common.csproj b/tests/Microsoft.Identity.Web.Test.Common/Microsoft.Identity.Web.Test.Common.csproj
index 455926183..5476bb2ea 100644
--- a/tests/Microsoft.Identity.Web.Test.Common/Microsoft.Identity.Web.Test.Common.csproj
+++ b/tests/Microsoft.Identity.Web.Test.Common/Microsoft.Identity.Web.Test.Common.csproj
@@ -1,4 +1,4 @@
-
+
net462; net472; net6.0; net8.0; net9.0
@@ -6,6 +6,7 @@
false
true
../../build/MSAL.snk
+ false
@@ -15,6 +16,7 @@
+
diff --git a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpClientFactory.cs b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpClientFactory.cs
index fbb5dccda..6ae107b82 100644
--- a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpClientFactory.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpClientFactory.cs
@@ -8,6 +8,7 @@
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using Microsoft.Identity.Client;
+using NSubstitute.Routing.Handlers;
using Xunit;
namespace Microsoft.Identity.Web.Test.Common.Mocks
@@ -15,41 +16,40 @@ namespace Microsoft.Identity.Web.Test.Common.Mocks
///
/// HttpClient that serves Http responses for testing purposes. Instance Discovery is added by default.
///
- public class MockHttpClientFactory : IMsalHttpClientFactory, IDisposable
+ ///
+ /// This implements the both IHttpClientFactory, which is what ID.Web uses. And IMsalHttpClientFactory which is what MSAL uses.
+ ///
+ public class MockHttpClientFactory : IMsalHttpClientFactory, IHttpClientFactory, IDisposable
{
- ///
- public void Dispose()
- {
- // This ensures we only check the mock queue on dispose when we're not in the middle of an
- // exception flow. Otherwise, any early assertion will cause this to likely fail
- // even though it's not the root cause.
-#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine
- if (Marshal.GetExceptionCode() == 0)
-#pragma warning restore CS0618 // Type or member is obsolete
- {
- string remainingMocks = string.Join(
- " ",
- _httpMessageHandlerQueue.Select(
- h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty));
+ private LinkedList _httpMessageHandlerQueue = new();
- Assert.Empty(_httpMessageHandlerQueue);
- }
- }
+ private volatile bool _addInstanceDiscovery = true;
public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
{
- _httpMessageHandlerQueue.Enqueue(handler);
+ if (_httpMessageHandlerQueue.Count == 0 && _addInstanceDiscovery)
+ {
+ _addInstanceDiscovery = false;
+ handler.ReplaceMockHttpMessageHandler = (h) =>
+ {
+ return _httpMessageHandlerQueue.AddFirst(h).Value;
+ };
+ }
+
+ // add a message to the front of the queue
+ _httpMessageHandlerQueue.AddLast(handler);
return handler;
}
- private Queue _httpMessageHandlerQueue = new Queue();
public HttpClient GetHttpClient()
{
- HttpMessageHandler messageHandler;
-
- Assert.NotEmpty(_httpMessageHandlerQueue);
- messageHandler = _httpMessageHandlerQueue.Dequeue();
+ HttpMessageHandler? messageHandler = _httpMessageHandlerQueue.First?.Value;
+ if (messageHandler == null)
+ {
+ throw new InvalidOperationException("The mock HTTP message handler queue is empty.");
+ }
+ _httpMessageHandlerQueue.RemoveFirst();
var httpClient = new HttpClient(messageHandler);
@@ -58,5 +58,30 @@ public HttpClient GetHttpClient()
return httpClient;
}
+
+ public HttpClient CreateClient(string name)
+ {
+ return GetHttpClient();
+ }
+
+
+ ///
+ public void Dispose()
+ {
+ // This ensures we only check the mock queue on dispose when we're not in the middle of an
+ // exception flow. Otherwise, any early assertion will cause this to likely fail
+ // even though it's not the root cause.
+#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine
+ if (Marshal.GetExceptionCode() == 0)
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ string remainingMocks = string.Join(
+ " ",
+ _httpMessageHandlerQueue.Select(
+ h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty));
+
+ Assert.Empty(_httpMessageHandlerQueue);
+ }
+ }
}
}
diff --git a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpMessageHandler.cs b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpMessageHandler.cs
index 488947ae5..064ac8b0d 100644
--- a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpMessageHandler.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpMessageHandler.cs
@@ -7,14 +7,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Azure.Core;
using Xunit;
namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public class MockHttpMessageHandler : HttpMessageHandler
{
- public Func ReplaceMockHttpMessageHandler;
+ internal Func ReplaceMockHttpMessageHandler { get; set; }
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public MockHttpMessageHandler()
@@ -32,6 +31,8 @@ public MockHttpMessageHandler()
public Exception ExceptionToThrow { get; set; }
+ public string ResponseString { get; private set; }
+
///
/// Once the http message is executed, this property holds the request message.
///
@@ -66,6 +67,8 @@ protected override async Task SendAsync(HttpRequestMessage
Content = new StringContent(TestConstants.DiscoveryJsonResponse),
};
+ ResponseString = TestConstants.DiscoveryJsonResponse;
+
return responseMessage;
}
@@ -82,7 +85,9 @@ protected override async Task SendAsync(HttpRequestMessage
Assert.Equal(ExpectedMethod, request.Method);
await ValidatePostDataAsync(request);
-
+
+ // Read the content of ResponseMessage.Content into a string variable
+ ResponseString = await ResponseMessage.Content.ReadAsStringAsync();
return ResponseMessage;
}
diff --git a/tests/E2E Tests/TokenAcquirerTests/OnlyOnAzureDevopsFactAttribute.cs b/tests/Microsoft.Identity.Web.Test.Common/OnlyOnAzureDevopsFactAttribute.cs
similarity index 90%
rename from tests/E2E Tests/TokenAcquirerTests/OnlyOnAzureDevopsFactAttribute.cs
rename to tests/Microsoft.Identity.Web.Test.Common/OnlyOnAzureDevopsFactAttribute.cs
index 80f98ec0f..b00eea158 100644
--- a/tests/E2E Tests/TokenAcquirerTests/OnlyOnAzureDevopsFactAttribute.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/OnlyOnAzureDevopsFactAttribute.cs
@@ -1,10 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using System;
using Xunit;
-namespace TokenAcquirerTests
+namespace Microsoft.Identity.Web.Test.Common
{
public sealed class OnlyOnAzureDevopsFactAttribute : FactAttribute
{
diff --git a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
index d9fe04f34..536f7c211 100644
--- a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
@@ -5,9 +5,11 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
+using Xunit;
namespace Microsoft.Identity.Web.Test.Common
{
+
public static class TestConstants
{
public const string ProductionPrefNetworkEnvironment = "login.microsoftonline.com";
diff --git a/tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs
index 967112d24..f20d3716f 100644
--- a/tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs
+++ b/tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs
@@ -316,9 +316,6 @@ private static async Task CreateAppAndGetTokenAsync(
if (addTokenMock)
{
mockHttp.AddMockHandler(tokenHandler);
-
- //Enables the mock handler to requeue requests that have been intercepted for instance discovery for example
- tokenHandler.ReplaceMockHttpMessageHandler = mockHttp.AddMockHandler;
}
var confidentialApp = ConfidentialClientApplicationBuilder
@@ -355,7 +352,6 @@ private static async Task CreateAppAndGetTokenAsync(
var result = await confidentialApp.AcquireTokenForClient(new[] { TestConstants.s_scopeForApp })
.ExecuteAsync();
- tokenHandler.ReplaceMockHttpMessageHandler = null!;
return result;
}
diff --git a/tests/Microsoft.Identity.Web.Test/MsAuth10AtPopTests.cs b/tests/Microsoft.Identity.Web.Test/MsAuth10AtPopTests.cs
index 4473a429c..85757a5e1 100644
--- a/tests/Microsoft.Identity.Web.Test/MsAuth10AtPopTests.cs
+++ b/tests/Microsoft.Identity.Web.Test/MsAuth10AtPopTests.cs
@@ -23,12 +23,8 @@ public async Task MsAuth10AtPop_WithAtPop_ShouldPopulateBuilderWithProofOfPosess
// Arrange
using MockHttpClientFactory mockHttpClientFactory = new MockHttpClientFactory();
using var httpTokenRequest = MockHttpCreator.CreateClientCredentialTokenHandler();
- //mockHttpClientFactory.AddMockHandler(MockHttpCreator.CreateInstanceDiscoveryMockHandler());
mockHttpClientFactory.AddMockHandler(httpTokenRequest);
- //Enables the mock handler to requeue requests that have been intercepted for instance discovery for example
- httpTokenRequest.ReplaceMockHttpMessageHandler = mockHttpClientFactory.AddMockHandler;
-
var certificateDescription = CertificateDescription.FromBase64Encoded(
TestConstants.CertificateX5cWithPrivateKey,
TestConstants.CertificateX5cWithPrivateKeyPassword);