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);