简体   繁体   中英

Proper way to use Azure Keyvault with Dependency Injection

Information I am building an Azure hosted protected API. I am targeting .Net 5 and using the MVC pattern. Inside this API, I have two DAL (data access layer) classes, which are added to the service container via the Startup.cs file. Both of these classes are Transient and expect a connection string in their constructors.

Problem I want to move the connection strings from the appsetting.json file of the API to the Azure key vault, but I only want to have to pull down the connection strings once from the Azure key vault and use them as static values to initialize any instances of the DAL classes. I've tried calling the respective method of the SecretClient class called GetSecretAsync and I use the constructor of SecretClient(Uri, ManagedIdentityCredential) . This all works when I call this after Startup.cs has ran, but when I try to do it inside of the Startup.cs it seems to hang for some unknown reason. I know that the calls do work later in the pipeline as I've tested them in the API as well as in the Azure Portal. This is what I've been trying to do via code:

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {

        //For Production, set the properties of the KeyVault 
        Secrets.SetProps(new KeyVaultHelper(Configuration)).GetAwaiter().GetResult();
        //Get the AzureAd section
        try
        {
            //Instead of storing the AzureAdBinding info in appsettings, I created a
            //json string which deserializes into a Dictionary which is used to
            //create the ConfigurationSection object
            Dictionary<string, string> azureadkvps = JsonConvert.DeserializeObject<Dictionary<string, string>>(Secrets.AzureAdBinding);
            //Turn it into an IConfigurationSection
            IConfigurationSection azureconfigsection = new ConfigurationBuilder().AddInMemoryCollection(azureadkvps).Build().GetSection("AzureAd");

            //Bind authentication to Configuration AzureAd section
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                   .AddMicrosoftIdentityWebApi(azureconfigsection);
        }
        catch { }

Secrets.cs

/// <summary>
/// Static class to hold values of keys pulled from Azure key vault
/// </summary>
public static class Secrets
{
    private const string MOBILE_APP_ID = "xxxxxxxxxx";
    private const string B_DB_CONN = "xxxxxxxxxx";
    private const string MOBILE_ADMIN_ROLE = "xxxxxxxxxx";
    private const string MOBILE_API_SCOPES = "xxxxxxxxxx";
    private const string MOBILE_COMMON_ROLE = "xxxxxxxxxx";
    private const string BL_DB_CONN = "xxxxxxxxxx";
    private const string AZUREAD_BINDING = "xxxxxxxxxx";

    public static string BLConn { get; private set; } = null;
    public static string BConn { get; private set; } = null;

    public static string MobileAdminRole { get; private set; } = null;

    public static string MobileUserRole { get; private set; } = null;

    public static string MobileApiScopes { get; private set; } = null;

    public static string MobileClientID { get; private set; } = null;

    public static string AzureAdBinding { get; private set; } = null;

    public async static Task SetProps(IKeyVaultHelp keyvault)
    {
        Task<string>[] secretretrievaltasks = new Task<string>[7];
        secretretrievaltasks[0] = keyvault.GetSecretAsync(MOBILE_APP_ID);
        secretretrievaltasks[1] = keyvault.GetSecretAsync(B_DB_CONN);
        secretretrievaltasks[2] = keyvault.GetSecretAsync(MOBILE_ADMIN_ROLE);
        secretretrievaltasks[3] = keyvault.GetSecretAsync(MOBILE_API_SCOPES);
        secretretrievaltasks[4] = keyvault.GetSecretAsync(MOBILE_COMMON_ROLE);
        secretretrievaltasks[5] = keyvault.GetSecretAsync(BL_DB_CONN);
        secretretrievaltasks[6] = keyvault.GetSecretAsync(AZUREAD_BINDING);
        await Task.WhenAll(secretretrievaltasks).ConfigureAwait(false);

        BLConn = secretretrievaltasks[5].Result;
        BConn = secretretrievaltasks[1].Result;
        MobileAdminRole = secretretrievaltasks[2].Result;
        MobileUserRole = secretretrievaltasks[4].Result;
        MobileApiScopes = secretretrievaltasks[3].Result;
        MobileClientID = secretretrievaltasks[0].Result;
        AzureAdBinding = secretretrievaltasks[6].Result;
    }
}

KeyVaultHelper.cs

 public class KeyVaultHelper : IKeyVaultHelp
 {
    private readonly IConfiguration _config;

    public KeyVaultHelper(IConfiguration config)
    {
        _config = config;
    }

    public string GetSecret(string secretkey)
    {
        string kvuri = $"https://{_config.GetValue<string>("KVN")}.vault.azure.net";
        var secretclient = new SecretClient(new Uri(kvuri), new ManagedIdentityCredential("a7cccd2b-fbf2-4b94-bda4-cd726ac80522"));
        return secretclient.GetSecret(secretkey).Value.Value;
    }

    public async Task<string> GetSecretAsync(string secretkey)
    {
        string kvuri = $"https://{_config.GetValue<string>("KVN")}.vault.azure.net";
        var secretclient = new SecretClient(new Uri(kvuri), new ManagedIdentityCredential());
        return (await secretclient.GetSecretAsync(secretkey).ConfigureAwait(false)).Value.Value;
    }
}

Perhaps I am going about this all wrong, but I'd like to be able to pull these values before adding the DAL classes to the service container so that the static values of the Secrets.cs hold the correct connection strings which will be used to construct the DAL objects.

I understand there is a way to add a Configuration file in Azure which can be built in Program.cs via the following, but I was hoping to just pull the values directly from KeyVault and use them throughout the lifetime of the application. Any direction or advice is appreciated.

    webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
    {
       var settings = config.Build();

       config.AddAzureAppConfiguration(options =>
       {
    
           options.Connect(settings["ConnectionStrings:AppConfig"])
              .ConfigureKeyVault(kv =>
              {
                  kv.SetCredential(new DefaultAzureCredential()); 
              });
          });
       })
       .UseStartup<Startup>());
    }

Thanks Camilo Terevinto , your comments are converting to an answer. So that it may helps to other community members to find this correct solution.

Instead in the Startup, use await with async Task Main in Program class where you already have the IConfiguration configured.

  • use AddAzureAppConfiguration if you're using app services, or
  • put the secrets in environment variables if you're running on VMs/containers

To populate the static values before the call to CreateHostBuilder(args).Build().Run() , you should call the async or await method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM