File: MicrosoftIdentityWebConfigurationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.EntraId.Tests\Aspire.Hosting.Azure.EntraId.Tests.csproj (Aspire.Hosting.Azure.EntraId.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
 
namespace Aspire.Hosting.Azure.EntraId.Tests;
 
public class MicrosoftIdentityWebConfigurationTests
{
    private const string TenantId = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
    private const string ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
    private const string ManagedIdentityClientId = "11112222-bbbb-3333-cccc-4444dddd5555";
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run, "AzureAd")]
    [InlineData(DistributedApplicationOperation.Publish, "AzureAd")]
    [InlineData(DistributedApplicationOperation.Run, "Authentication:Entra")]
    [InlineData(DistributedApplicationOperation.Publish, "Authentication:Entra")]
    public async Task WithReference_BindsToMicrosoftIdentityWebOptions(
        DistributedApplicationOperation operation,
        string sectionName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var secret = builder.AddParameter("EntraSecret", "test-client-secret", secret: true);
        var password = builder.AddParameter("CertificatePassword", "test-certificate-password", secret: true);
        var entra = builder.AddExistingEntraApplication("entra-web", tenantId: TenantId, clientId: ClientId)
            .WithInstance("https://login.microsoftonline.us/")
            .WithSignInAudience(EntraIdSignInAudience.AzureADMultipleOrgs)
            .WithClientSecret(secret)
            .WithCredential(new EntraIdFederatedIdentityCredential
            {
                ManagedIdentityClientId = ManagedIdentityClientId,
                TokenExchangeUrl = "api://AzureADTokenExchangeUSGov",
                TokenExchangeAuthority = $"https://login.microsoftonline.us/{TenantId}"
            })
            .WithCertificateFromKeyVault("https://contoso.vault.usgovcloudapi.net", "web-certificate")
            .WithCertificateThumbprint("CurrentUser/My", "ABC123")
            .WithCertificateDistinguishedName("LocalMachine/My", "CN=Contoso")
            .WithCredential(new EntraIdFileCertificateCredential
            {
                FilePath = "/certificates/web.pfx",
                Password = password.Resource
            })
            .WithCredential(new EntraIdSignedAssertionFileCredential { FilePath = "/var/run/secrets/assertion" })
            .WithClientCapability("cp1")
            .WithAudience($"api://{ClientId}")
            .WithSendX5C()
            .WithAllowWebApiToBeAuthorizedByACL()
            .WithExtraQueryParameter("loginHint", "user@contoso.example");
        var project = builder.AddResource(new ProjectResource("web"))
            .WithReference(entra, connectionName: sectionName);
 
        var configuration = await GetConfigurationAsync(project.Resource, operation);
        var section = configuration.GetSection(sectionName);
        // The unified options cover settings shared by Identity.Web's authentication and token-acquisition options.
        // Strict binding catches misspelled credential keys instead of silently accepting unknown configuration.
        var options = section.Get<MicrosoftEntraApplicationOptions>(binder => binder.ErrorOnUnknownConfiguration = true);
        Assert.NotNull(options);
        Assert.Equal("https://login.microsoftonline.us/", options.Instance);
        Assert.Equal("organizations", options.TenantId);
        Assert.Equal(TenantId, options.AppHomeTenantId);
        Assert.Equal(ClientId, options.ClientId);
        Assert.True(options.SendX5C);
        Assert.True(options.AllowWebApiToBeAuthorizedByACL);
        Assert.Equal(["cp1"], options.ClientCapabilities);
        Assert.Equal([$"api://{ClientId}"], options.Audiences);
        Assert.Equal(new Dictionary<string, string> { ["loginHint"] = "user@contoso.example" }, options.ExtraQueryParameters);
 
        Assert.NotNull(options.ClientCredentials);
        Assert.Collection(options.ClientCredentials,
            credential =>
            {
                Assert.Equal(CredentialSource.ClientSecret, credential.SourceType);
                Assert.Equal(operation == DistributedApplicationOperation.Run ? "test-client-secret" : "{EntraSecret.value}", credential.ClientSecret);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.SignedAssertionFromManagedIdentity, credential.SourceType);
                Assert.Equal(ManagedIdentityClientId, credential.ManagedIdentityClientId);
                Assert.Equal("api://AzureADTokenExchangeUSGov", credential.TokenExchangeUrl);
                Assert.Equal($"https://login.microsoftonline.us/{TenantId}", credential.TokenExchangeAuthority);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.KeyVault, credential.SourceType);
                Assert.Equal("https://contoso.vault.usgovcloudapi.net", credential.KeyVaultUrl);
                Assert.Equal("web-certificate", credential.KeyVaultCertificateName);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.StoreWithThumbprint, credential.SourceType);
                Assert.Equal("CurrentUser/My", credential.CertificateStorePath);
                Assert.Equal("ABC123", credential.CertificateThumbprint);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.StoreWithDistinguishedName, credential.SourceType);
                Assert.Equal("LocalMachine/My", credential.CertificateStorePath);
                Assert.Equal("CN=Contoso", credential.CertificateDistinguishedName);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.Path, credential.SourceType);
                Assert.Equal("/certificates/web.pfx", credential.CertificateDiskPath);
                Assert.Equal(operation == DistributedApplicationOperation.Run ? "test-certificate-password" : "{CertificatePassword.value}", credential.CertificatePassword);
            },
            credential =>
            {
                Assert.Equal(CredentialSource.SignedAssertionFilePath, credential.SourceType);
                Assert.Equal("/var/run/secrets/assertion", credential.SignedAssertionFileDiskPath);
            });
 
        var webOptions = section.Get<MicrosoftIdentityOptions>();
        Assert.NotNull(webOptions);
        Assert.Equal(options.ClientId, webOptions.ClientId);
        Assert.Equal(options.Instance, webOptions.Instance);
        Assert.Equal(options.TenantId, webOptions.TenantId);
        Assert.Equal(options.ClientCredentials.Select(credential => credential.SourceType),
            webOptions.ClientCredentials!.Select(credential => credential.SourceType));
    }
 
    [Theory]
    [InlineData(EntraIdSignInAudience.AzureADMyOrg, null)]
    [InlineData(EntraIdSignInAudience.AzureADMultipleOrgs, "organizations")]
    [InlineData(EntraIdSignInAudience.AzureADandPersonalMicrosoftAccount, "common")]
    [InlineData(EntraIdSignInAudience.PersonalMicrosoftAccount, "consumers")]
    public async Task WithReference_CreatedRegistration_BindsTenantAndClientOutputs(
        EntraIdSignInAudience audience,
        string? signInTenant)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var entra = builder.AddEntraIdApplication("entra-web").WithSignInAudience(audience);
        entra.Resource.Outputs["tenantId"] = TenantId;
        entra.Resource.Outputs["clientId"] = ClientId;
        entra.Resource.ProvisioningTaskCompletionSource!.SetResult();
        var project = builder.AddResource(new ProjectResource("web")).WithReference(entra);
 
        var configuration = await GetConfigurationAsync(project.Resource, DistributedApplicationOperation.Run);
        var options = configuration.GetSection("AzureAd")
            .Get<MicrosoftEntraApplicationOptions>(binder => binder.ErrorOnUnknownConfiguration = true);
 
        Assert.NotNull(options);
        Assert.Equal(ClientId, options.ClientId);
        Assert.Equal(signInTenant ?? TenantId, options.TenantId);
        Assert.Equal(signInTenant is null ? null : TenantId, options.AppHomeTenantId);
        Assert.True(options.ClientCredentials is null || !options.ClientCredentials.Any());
    }
 
    [Fact]
    public async Task WithReference_ConfiguresMicrosoftIdentityWebSignInOnly()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var entra = builder.AddExistingEntraApplication("entra-web", tenantId: TenantId, clientId: ClientId);
        var project = builder.AddResource(new ProjectResource("web")).WithReference(entra);
        var configuration = await GetConfigurationAsync(project.Resource, DistributedApplicationOperation.Run);
        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(configuration);
        services.AddLogging();
        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"));
        using var provider = services.BuildServiceProvider();
 
        var options = provider.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>()
            .Get(OpenIdConnectDefaults.AuthenticationScheme);
 
        Assert.Equal("id_token", options.ResponseType);
        Assert.Equal("/signin-oidc", options.CallbackPath.Value);
        Assert.Equal("/signout-callback-oidc", options.SignedOutCallbackPath.Value);
        Assert.True(options.RequireHttpsMetadata);
        Assert.Equal(ClientId, options.ClientId);
    }
 
    private static async Task<IConfigurationRoot> GetConfigurationAsync(
        IResource resource,
        DistributedApplicationOperation operation)
    {
        var environment = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, operation, TestServiceProvider.Instance);
        return new ConfigurationBuilder()
            .AddInMemoryCollection(environment.Select(entry =>
                new KeyValuePair<string, string?>(entry.Key.Replace("__", ":", StringComparison.Ordinal), entry.Value)))
            .Build();
    }
}