2

We've uploaded my ABP Framework site to an Azure Web application but the default connection string is stored inside the configuration of the web app. Now we want to replace that to an Azure Key Vault and store only the URL inside the configuration.

Where using this code:

using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using System;

namespace OurNamespace.Utils
{
    public static class AppAzureKeyVaultConfigurer
    {
        public static IConfigurationBuilder ConfigureAzureKeyVault(this IConfigurationBuilder builder, string azureKeyVaultUrl)
        {
            SecretClient keyVaultClient = new SecretClient(
                new Uri(azureKeyVaultUrl),
                new DefaultAzureCredential()
            );

            AzureKeyVaultConfigurationOptions options = new AzureKeyVaultConfigurationOptions()
            {
                ReloadInterval = TimeSpan.FromHours(1)
            };

            return builder.AddAzureKeyVault(keyVaultClient, options);
        }
    }
}

Inside the Program class of the OurNamespace.HttpApi.Host project, next code will be called:

internal static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .AddAppSettingsSecretsJson()
        .ConfigureAppConfiguration(build =>
        {
            IConfigurationBuilder configuration = build
                .AddJsonFile("appsettings.secrets.json", optional: true)
                .ConfigureAzureKeyVault("☺ --> the url to the key vault");
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseAutofac()
        .UseSerilog();

To get the assess token inside the OurApplicationDbContext:

[ReplaceDbContext(typeof(IIdentityProDbContext), typeof(ISaasDbContext), typeof(ILanguageManagementDbContext), typeof(IAuditLoggingDbContext), typeof(ITextTemplateManagementDbContext), typeof(IIdentityServerDbContext), typeof(IPaymentDbContext), typeof(IPermissionManagementDbContext), typeof(ISettingManagementDbContext), typeof(IFeatureManagementDbContext), typeof(IBackgroundJobsDbContext), typeof(IBlobStoringDbContext))]
[ConnectionStringName("Default")]
public class OurApplicationDbContext : AbpDbContext<OurApplicationDbContext>, IOurApplicationDbContext, IIdentityProDbContext, ISaasDbContext, ILanguageManagementDbContext, IAuditLoggingDbContext, ITextTemplateManagementDbContext, IIdentityServerDbContext, IPaymentDbContext, IPermissionManagementDbContext, ISettingManagementDbContext, IFeatureManagementDbContext, IBackgroundJobsDbContext, IBlobStoringDbContext
{
    private readonly IConfiguration _configuration;

    // All implementations of all interfaces here

    public OurApplicationDbContext(DbContextOptions<OurApplicationDbContext> options, IConfiguration configuration)
        : base(options)
    {
        _configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured) // <-- also a break point here will not be hit.
            optionsBuilder.UseSqlServer(_configuration.GetConnectionString("Default"));
    }
}

Inside the appsettings.json the Connectionstring:Default is removed because it must be taken from the Key Vault.

Also in the DbContext of the OurNamespace.EntityFrameworkCore project, all these DbContexts are replaced:

  • IIdentityProDbContext
  • ISaasDbContext
  • ILanguageManagementDbContext
  • IAuditLoggingDbContext
  • ITextTemplateManagementDbContext
  • IIdentityServerDbContext
  • IPaymentDbContext
  • IPermissionManagementDbContext
  • ISettingManagementDbContext
  • IFeatureManagementDbContext
  • IBackgroundJobsDbContext
  • IBlobStoringDbContext

It will give next error:

ArgumentNullException: Value cannot be null. (Parameter connectionString)

DependencyResolutionException: An exception was thrown while activating: Volo.Abp.LanguageManagement.EntityFrameworkCore.ILanguageManagementDbContextOurNamespace.EntityFrameworkCore.OurApplicationDbContextMicrosoft.EntityFrameworkCore.DbContextOptions<OurNamespace.EntityFrameworkCore.OurApplicationDbContext>.

Update

If I do nothing (placing my connection string key inside the appsettings.json file), the ILanguageManagementDbContext will work as expected. Also other keys from the Key Vault will be taken. Also checked that the correct key is stores inside Key Vault and didn't find any problems.

Specifications

  • ABP Framework version: 5.0.0
  • UI type: Angular
  • DB provider: EF Core
H. Pauwelyn
  • 13,575
  • 26
  • 81
  • 144
  • Can I understand your requirement as "you failed to get the secrets stored in azure key vault"? – Tiny Wang Apr 13 '22 at 07:40
  • @TinyWang: It's looks like the code is not going to the Key Vault. – H. Pauwelyn Apr 13 '22 at 07:45
  • Thank you sir, per my understanding, you want to store the connection string of database into azure key vault, so when you start your project, you need to get the connection string. Therefore, what you need is a solution to get azure key vault secrets... – Tiny Wang Apr 13 '22 at 07:47
  • @TinyWang: We have the code see first code block from question. To get the access token we override the `OnConfiguring`. – H. Pauwelyn Apr 13 '22 at 07:51
  • I think your issue may come from the `DefaultAzureCredential`. When we use azure key vault, we need to set access policy so that some user or some azure web application can access this key vault. For example, you add user1@xx.onmicrosoft.com into the access policy, this means when you test in your local visual studio, you can use this user to sign in VS, then `DefaultAzureCredential` can use it as the credential provider to ask for resource for the key vault. – Tiny Wang Apr 13 '22 at 09:13
  • `DefaultAzureCredential` can also get credential from environement variables, and [environment variables](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme#environment-variables) has a higher priority than VS, like what I showed in the screenshot below. – Tiny Wang Apr 13 '22 at 09:15
  • Can my post help you sir? – Tiny Wang Apr 14 '22 at 01:27
  • @TinyWang: Sorry for my late reaction. Your answer is not fixing the issue I got. It's giving the same error even if I use this: `services.AddDbContext(options =>options.UseSqlServer(d));`. – H. Pauwelyn Apr 22 '22 at 06:57
  • Firstly, we need to make sure if we configured azure key vault correctly, in my code, when I add breakpoint at line `var a = Configuration.GetSection("LocalDbConnectionString");` I can see the secrets stored in azure key vault, then I used the connection to init database connection. My question is, did you get the secrets stored in azure key vault yet? – Tiny Wang Apr 22 '22 at 07:15
  • If you hasn't got the secrets, I'm afraid you'd better to check the code first, if code is correct(azure key vault configutaion codes), you also need to check if there're `AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET` environment variables in your computer and check if the user account you signed in VS is consented to access azure key vault/ – Tiny Wang Apr 22 '22 at 07:19

1 Answers1

1

Follow this document, it showed when we want to add azure keyvault secrets into configuration, we can use managed identities for getting azure resources. That is what OP used DefaultAzureCredential. Using DefaultAzureCredential required to do some configuration to make sure we've provided credential for our application. For local test, we used visual studio, so we can use the account which has azure key vault access permission to sign in visual studio, this can be one of the credential. And if we've published the application to azure app service, then also add the web app service princple to the key vault access policy.

enter image description here

Here's my sample code.

My program.cs, add ConfigureAppConfiguration:

using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;

namespace WebMvcAppLinkDb
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, config) =>
                {
                    var builtConfig = config.Build();
                    var secretClient = new SecretClient(
                        new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
                        new DefaultAzureCredential());
                    config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

My startup.cs,

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    //use connection string stored in appsetting
    //services.AddDbContext<MyDbContext>(options =>options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
    //use connection string stored in azure key vault
    //after adding Azure Key Vault configuration provider, we debug code here, and will see Cigfiguration has one more provider from keyvault
    var a = Configuration.GetSection("LocalDbConnectionString");
    var b = a.Value;
    var c = b.ToString();
    //I stored connection string with name "LocalDbConnectionString" in azure keyvault, but when I get the value from key vault
    //I don't know why the data format like Server=(localdb)\\\\xxdb, so I need to remove \\
    var d = c.Remove(16,1);
    services.AddDbContext<MyDbContext>(options =>options.UseSqlServer(d));
}

My appsetting:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  //"ConnectionStrings": {
  //  "MvcMovieContext": "Server=(localdb)\\xxdb;Database=xx;Trusted_Connection=True;MultipleActiveResultSets=true"
  //},
  "KeyVaultName": "my_keyvault_name"
}
Tiny Wang
  • 10,423
  • 1
  • 11
  • 29