File: EntraIdApplicationResource.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.EntraId\Aspire.Hosting.Azure.EntraId.csproj (Aspire.Hosting.Azure.EntraId)
// 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;
 
namespace Aspire.Hosting.Azure;
 
/// <summary>
/// A resource that represents a Microsoft Entra ID application registration.
/// </summary>
/// <remarks>
/// <para>
/// Entra ID application registrations define the identity configuration for services
/// in a distributed application. Each application registration includes a tenant ID,
/// client ID, and optionally client credentials and API scopes.
/// </para>
/// <para>
/// References to this resource inject configuration as environment variables.
/// .NET program references default to names such as <c>AzureAd__TenantId</c> and <c>AzureAd__ClientId</c>,
/// compatible with Microsoft.Identity.Web. Other references default to an uppercase resource-name prefix
/// and a single underscore separator, and can configure their own prefix and separator.
/// </para>
/// <para>
/// The properties align with <c>MicrosoftEntraApplicationOptions</c> from
/// <c>Microsoft.Identity.Abstractions</c>.
/// </para>
/// </remarks>
public class EntraIdApplicationResource : Resource
{
    private const string DefaultInstance = "https://login.microsoftonline.com/";
 
    /// <summary>
    /// Initializes a new instance of the <see cref="EntraIdApplicationResource"/> class.
    /// </summary>
    /// <param name="name">The name of the resource.</param>
    public EntraIdApplicationResource(string name)
        : base(name)
    {
    }
 
    // ── Core identity ──────────────────────────────────────────────────────
 
    /// <summary>
    /// Gets or sets the Entra ID instance URL.
    /// </summary>
    /// <remarks>
    /// Defaults to <c>https://login.microsoftonline.com/</c>. Override for sovereign clouds
    /// (e.g., <c>https://login.microsoftonline.us/</c> for Azure Government).
    /// </remarks>
    public string Instance { get; set; } = DefaultInstance;
 
    /// <summary>
    /// Gets or sets the parameter resource for the ID of the app's home tenant.
    /// </summary>
    /// <remarks>
    /// The home tenant is the directory where the app is registered, shown as "Directory (tenant) ID"
    /// on the app registration's Overview page.
    /// </remarks>
    public ParameterResource? TenantIdParameter { get; set; }
 
    /// <summary>
    /// Gets or sets a fixed ID of the app's home tenant.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The home tenant is the directory where the app is registered, shown as "Directory (tenant) ID"
    /// on the app registration's Overview page. It can be a GUID or a domain such as <c>contoso.onmicrosoft.com</c>.
    /// </para>
    /// <para>
    /// When both <see cref="TenantIdParameter"/> and <see cref="TenantId"/> are set,
    /// <see cref="TenantIdParameter"/> takes precedence.
    /// </para>
    /// </remarks>
    public string? TenantId { get; set; }
 
    /// <summary>
    /// Gets or sets the parameter resource for the client ID.
    /// </summary>
    public ParameterResource? ClientIdParameter { get; set; }
 
    /// <summary>
    /// Gets or sets a fixed client ID value.
    /// </summary>
    /// <remarks>
    /// When both <see cref="ClientIdParameter"/> and <see cref="ClientId"/> are set,
    /// <see cref="ClientIdParameter"/> takes precedence.
    /// </remarks>
    public string? ClientId { get; set; }
 
    /// <summary>
    /// Gets or sets which accounts can sign in to the application.
    /// </summary>
    /// <remarks>
    /// Defaults to <see cref="EntraIdSignInAudience.AzureADMyOrg"/>, which allows only accounts in the home tenant.
    /// The value must match the "Supported account types" setting of the app registration.
    /// </remarks>
    public EntraIdSignInAudience SignInAudience { get; set; } = EntraIdSignInAudience.AzureADMyOrg;
 
    /// <summary>
    /// Gets the keyword that Microsoft.Identity.Web expects as <c>TenantId</c> for <see cref="SignInAudience"/>,
    /// or <see langword="null"/> when the app is single-tenant and uses its home tenant instead.
    /// </summary>
    /// <remarks>
    /// For more information about the keywords, see
    /// <a href="https://learn.microsoft.com/entra/identity-platform/v2-protocols#endpoints">Microsoft identity platform endpoints</a>.
    /// </remarks>
    internal string? SignInTenantKeyword => SignInAudience switch
    {
        EntraIdSignInAudience.AzureADMyOrg => null,
        EntraIdSignInAudience.AzureADMultipleOrgs => "organizations",
        EntraIdSignInAudience.AzureADandPersonalMicrosoftAccount => "common",
        EntraIdSignInAudience.PersonalMicrosoftAccount => "consumers",
        _ => throw new InvalidOperationException($"'{SignInAudience}' is not a valid {nameof(EntraIdSignInAudience)} value.")
    };
 
    // ── Token acquisition ──────────────────────────────────────────────────
 
    /// <summary>
    /// Gets the list of client credentials configured for this application.
    /// </summary>
    /// <remarks>
    /// Supports multiple credential types used by Microsoft.Identity.Web:
    /// <list type="bullet">
    /// <item><description><c>ClientSecret</c> — application secret</description></item>
    /// <item><description><c>SignedAssertionFromManagedIdentity</c> — managed identity credential</description></item>
    /// <item><description><c>Certificate</c> — certificate-based credential</description></item>
    /// </list>
    /// </remarks>
    internal List<EntraIdClientCredential> ClientCredentials { get; } = [];
 
    /// <summary>
    /// Gets the client capabilities (e.g., <c>"cp1"</c> for Continuous Access Evaluation).
    /// </summary>
    internal List<string> ClientCapabilities { get; } = [];
 
    /// <summary>
    /// Gets or sets whether to send the <c>x5c</c> claim (the public key of the certificate) with the token request.
    /// </summary>
    /// <remarks>
    /// Sending <c>x5c</c> enables easy certificate rollover, but it is only meaningful for certificate-based
    /// credentials and increases request size, so it is opt-in via <c>WithSendX5C()</c>.
    /// </remarks>
    public bool SendX5C { get; set; }
 
    // ── Web API ────────────────────────────────────────────────────────────
 
    /// <summary>
    /// Gets the list of audiences accepted by this application registration.
    /// </summary>
    internal List<string> Audiences { get; } = [];
 
    /// <summary>
    /// Gets or sets whether to allow ACL-based authorization for daemon-to-API calls.
    /// </summary>
    /// <remarks>
    /// When <see langword="true"/>, the web API will not require roles or scopes in the token,
    /// allowing client credentials flow callers to be authorized by an Access Control List.
    /// </remarks>
    public bool AllowWebApiToBeAuthorizedByACL { get; set; }
 
    // ── Query parameters ───────────────────────────────────────────────────
 
    /// <summary>
    /// Gets extra query parameters to send to the identity provider.
    /// </summary>
    /// <remarks>
    /// Useful for routing to specific test slices or data centers.
    /// </remarks>
    internal Dictionary<string, string> ExtraQueryParameters { get; } = [];
}
 
/// <summary>
/// Base class for client credential entries in an Entra ID application registration.
/// </summary>
/// <remarks>
/// <para>
/// Each subclass corresponds to a specific <c>SourceType</c> in the Microsoft.Identity.Web
/// <c>ClientCredentials</c> configuration array. Use one of the concrete types:
/// </para>
/// <list type="bullet">
/// <item><description><see cref="EntraIdClientSecretCredential"/> — application secret.</description></item>
/// <item><description><see cref="EntraIdFederatedIdentityCredential"/> — FIC with managed identity.</description></item>
/// <item><description><see cref="EntraIdKeyVaultCertificateCredential"/> — certificate from Azure Key Vault.</description></item>
/// <item><description><see cref="EntraIdStoreCertificateCredential"/> — certificate from the certificate store.</description></item>
/// <item><description><see cref="EntraIdFileCertificateCredential"/> — certificate from a file on disk.</description></item>
/// <item><description><see cref="EntraIdSignedAssertionFileCredential"/> — signed assertion file (AKS workload identity).</description></item>
/// </list>
/// </remarks>
public abstract class EntraIdClientCredential
{
    /// <summary>
    /// Gets the <c>SourceType</c> string used in the Microsoft.Identity.Web configuration.
    /// </summary>
    public abstract string SourceType { get; }
 
    /// <summary>
    /// Emits environment variables for this credential into the given context.
    /// </summary>
    /// <param name="envVars">The environment variable dictionary to populate.</param>
    /// <param name="prefix">The environment variable prefix (e.g., <c>"AzureAd__ClientCredentials__0"</c>).</param>
    /// <param name="separator">The separator between configuration keys.</param>
    internal virtual void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        envVars[$"{prefix}{separator}SourceType"] = SourceType;
    }
}
 
/// <summary>
/// A client secret credential for an Entra ID application.
/// </summary>
/// <remarks>
/// Maps to <c>SourceType = "ClientSecret"</c> in Microsoft.Identity.Web configuration.
/// </remarks>
public sealed class EntraIdClientSecretCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    public override string SourceType => "ClientSecret";
 
    /// <summary>
    /// Gets the client secret as a parameter resource.
    /// </summary>
    public required ParameterResource ClientSecret
    {
        get;
        init
        {
            ArgumentNullException.ThrowIfNull(value);
            if (!value.Secret)
            {
                throw new ArgumentException("The client secret parameter must be marked as secret. Use AddParameter with secret: true when creating the parameter.", nameof(ClientSecret));
            }
 
            field = value;
        }
    }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
        envVars[$"{prefix}{separator}ClientSecret"] = ClientSecret;
    }
}
 
/// <summary>
/// A federated identity credential (FIC) with managed identity for an Entra ID application.
/// </summary>
/// <remarks>
/// Maps to <c>SourceType = "SignedAssertionFromManagedIdentity"</c> in Microsoft.Identity.Web configuration.
/// For system-assigned managed identity, leave <see cref="ManagedIdentityClientId"/> as <see langword="null"/>.
/// </remarks>
public sealed class EntraIdFederatedIdentityCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    public override string SourceType => "SignedAssertionFromManagedIdentity";
 
    /// <summary>
    /// Gets or sets the client ID of a user-assigned managed identity.
    /// </summary>
    /// <remarks>
    /// Leave <see langword="null"/> for system-assigned managed identity.
    /// </remarks>
    public string? ManagedIdentityClientId { get; set; }
 
    /// <summary>
    /// Gets or sets the token exchange URL for federated identity credential scenarios.
    /// </summary>
    /// <remarks>
    /// If not specified, defaults to <c>api://AzureADTokenExchange</c>.
    /// </remarks>
    public string? TokenExchangeUrl { get; set; }
 
    /// <summary>
    /// Gets or sets the token exchange authority URL.
    /// </summary>
    /// <remarks>
    /// Used when the issuer for token exchange differs from the application's authority.
    /// </remarks>
    public string? TokenExchangeAuthority { get; set; }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
 
        if (ManagedIdentityClientId is not null)
        {
            envVars[$"{prefix}{separator}ManagedIdentityClientId"] = ManagedIdentityClientId;
        }
 
        if (TokenExchangeUrl is not null)
        {
            envVars[$"{prefix}{separator}TokenExchangeUrl"] = TokenExchangeUrl;
        }
 
        if (TokenExchangeAuthority is not null)
        {
            envVars[$"{prefix}{separator}TokenExchangeAuthority"] = TokenExchangeAuthority;
        }
    }
}
 
/// <summary>
/// A certificate credential from Azure Key Vault for an Entra ID application.
/// </summary>
/// <remarks>
/// Maps to <c>SourceType = "KeyVault"</c> in Microsoft.Identity.Web configuration.
/// </remarks>
public sealed class EntraIdKeyVaultCertificateCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    public override string SourceType => "KeyVault";
 
    /// <summary>
    /// Gets the URL of the Azure Key Vault (e.g., <c>"https://myvault.vault.azure.net"</c>).
    /// </summary>
    public required string KeyVaultUrl { get; init; }
 
    /// <summary>
    /// Gets the name of the certificate in Azure Key Vault.
    /// </summary>
    public required string CertificateNameInKeyVault { get; init; }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
        envVars[$"{prefix}{separator}KeyVaultUrl"] = KeyVaultUrl;
        envVars[$"{prefix}{separator}KeyVaultCertificateName"] = CertificateNameInKeyVault;
    }
}
 
/// <summary>
/// A certificate credential from the certificate store for an Entra ID application.
/// </summary>
/// <remarks>
/// <para>
/// Maps to <c>SourceType = "StoreWithThumbprint"</c> or <c>"StoreWithDistinguishedName"</c>
/// in Microsoft.Identity.Web configuration, depending on which identifier is provided.
/// </para>
/// <para>
/// Set exactly one of <see cref="Thumbprint"/> or <see cref="DistinguishedName"/>. Setting both,
/// or neither, causes <see cref="SourceType"/> to throw an <see cref="InvalidOperationException"/>.
/// </para>
/// </remarks>
public sealed class EntraIdStoreCertificateCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    /// <exception cref="InvalidOperationException">
    /// Thrown when neither or both of <see cref="Thumbprint"/> and <see cref="DistinguishedName"/> are set.
    /// </exception>
    public override string SourceType => (Thumbprint, DistinguishedName) switch
    {
        (not null, null) => "StoreWithThumbprint",
        (null, not null) => "StoreWithDistinguishedName",
        (not null, not null) => throw new InvalidOperationException(
            $"Only one of {nameof(Thumbprint)} or {nameof(DistinguishedName)} can be set on {nameof(EntraIdStoreCertificateCredential)}."),
        _ => throw new InvalidOperationException(
            $"Either {nameof(Thumbprint)} or {nameof(DistinguishedName)} must be set on {nameof(EntraIdStoreCertificateCredential)}.")
    };
 
    /// <summary>
    /// Gets the certificate store path (e.g., <c>"CurrentUser/My"</c> or <c>"LocalMachine/My"</c>).
    /// </summary>
    public required string StorePath { get; init; }
 
    /// <summary>
    /// Gets or sets the certificate thumbprint.
    /// </summary>
    public string? Thumbprint { get; set; }
 
    /// <summary>
    /// Gets or sets the certificate distinguished name (e.g., <c>"CN=MyCert"</c>).
    /// </summary>
    public string? DistinguishedName { get; set; }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
        envVars[$"{prefix}{separator}CertificateStorePath"] = StorePath;
 
        if (Thumbprint is not null)
        {
            envVars[$"{prefix}{separator}CertificateThumbprint"] = Thumbprint;
        }
        else if (DistinguishedName is not null)
        {
            envVars[$"{prefix}{separator}CertificateDistinguishedName"] = DistinguishedName;
        }
    }
}
 
/// <summary>
/// A certificate credential from a file on disk for an Entra ID application.
/// </summary>
/// <remarks>
/// Maps to <c>SourceType = "Path"</c> in Microsoft.Identity.Web configuration.
/// Not recommended for production use.
/// </remarks>
public sealed class EntraIdFileCertificateCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    public override string SourceType => "Path";
 
    /// <summary>
    /// Gets the path to the certificate file (e.g., a PFX file).
    /// </summary>
    public required string FilePath { get; init; }
 
    /// <summary>
    /// Gets or sets the password for the certificate file, if password-protected.
    /// </summary>
    /// <remarks>
    /// This is a <see cref="ParameterResource"/> rather than a <see cref="string"/> so the value flows through
    /// Aspire's parameter pipeline and is redacted in logs and the dashboard like any other secret.
    /// The parameter must be created with <c>secret: true</c>.
    /// </remarks>
    public ParameterResource? Password
    {
        get;
        set
        {
            if (value is { Secret: false })
            {
                throw new ArgumentException("The certificate password parameter must be marked as secret. Use AddParameter with secret: true when creating the parameter.", nameof(Password));
            }
 
            field = value;
        }
    }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
        envVars[$"{prefix}{separator}CertificateDiskPath"] = FilePath;
 
        if (Password is not null)
        {
            envVars[$"{prefix}{separator}CertificatePassword"] = Password;
        }
    }
}
 
/// <summary>
/// A signed assertion file credential for an Entra ID application (e.g., AKS workload identity).
/// </summary>
/// <remarks>
/// Maps to <c>SourceType = "SignedAssertionFilePath"</c> in Microsoft.Identity.Web configuration.
/// If <see cref="FilePath"/> is not provided, the <c>AZURE_FEDERATED_TOKEN_FILE</c>
/// environment variable is used.
/// </remarks>
public sealed class EntraIdSignedAssertionFileCredential : EntraIdClientCredential
{
    /// <inheritdoc />
    public override string SourceType => "SignedAssertionFilePath";
 
    /// <summary>
    /// Gets or sets the path to the signed assertion file on disk.
    /// </summary>
    /// <remarks>
    /// If not specified, defaults to the <c>AZURE_FEDERATED_TOKEN_FILE</c> environment variable.
    /// </remarks>
    public string? FilePath { get; set; }
 
    /// <inheritdoc />
    internal override void EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator)
    {
        base.EmitEnvironmentVariables(envVars, prefix, separator);
 
        if (FilePath is not null)
        {
            envVars[$"{prefix}{separator}SignedAssertionFileDiskPath"] = FilePath;
        }
    }
}