File: EntraIdLocalDeveloperCertificateCredential.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;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
 
namespace Aspire.Hosting.Azure;
 
/// <summary>
/// Configures a local certificate without exposing its private material to provisioning.
/// </summary>
internal sealed class EntraIdLocalDeveloperCertificateCredential : EntraIdClientCredential
{
    private readonly DistributedApplicationExecutionContext _executionContext;
    // Bicep generation, provisioning, and each referencing resource's environment evaluation can request the
    // certificate concurrently. This ensures they share one selection (and one store call) for the run. The
    // store's file lock separately coordinates other AppHost processes.
    private readonly SemaphoreSlim _certificateLock = new(1, 1);
    private LocalDeveloperCertificate? _certificate;
 
    public EntraIdLocalDeveloperCertificateCredential(EntraIdApplicationResource application, DistributedApplicationExecutionContext executionContext)
    {
        _executionContext = executionContext;
        // A ParameterResource makes the PFX participate in the existing secret discovery/redaction
        // pipeline. It has no published default and never enters the Azure provisioning parameters.
        Secret = new ParameterResource(
            $"{application.Name}-local-certificate",
            _ => GetCertificate(application).Base64EncodedValue,
            secret: true);
    }
 
    public override string SourceType => "Base64Encoded";
 
    internal ParameterResource Secret { get; }
 
    internal LocalDeveloperCertificate GetCertificate(EntraIdApplicationResource application) =>
        GetCertificateAsync(_executionContext.Services, application,
            _executionContext.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping).GetAwaiter().GetResult();
 
    internal async Task<LocalDeveloperCertificate> GetCertificateAsync(IServiceProvider services, EntraIdApplicationResource application, CancellationToken cancellationToken)
    {
        await _certificateLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            // Keep one selection for the entire run. An environment callback evaluated a day after
            // provisioning must not silently switch to a certificate that Graph has not received.
            return _certificate ??= await services.GetRequiredService<LocalDeveloperCertificateStore>()
                .GetCertificateAsync(application.Name, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _certificateLock.Release();
        }
    }
 
    internal override bool EmitEnvironmentVariables(IDictionary<string, object> envVars, string prefix, string separator, EntraIdApplicationResource application, IResource consumer, DistributedApplicationExecutionContext executionContext)
    {
        if (executionContext.IsPublishMode)
        {
            return false;
        }
 
        base.EmitEnvironmentVariables(envVars, prefix, separator, application, consumer, executionContext);
        envVars[$"{prefix}{separator}Base64EncodedValue"] = Secret;
        return true;
    }
}