4
votes

I’m trying to use Azure Key Vault for storing my web api connection strings for Entity Framework. Ideally I’d like to avoid coupling the key vault nuget packages with my data access code. My dbContext class has two constructors:

public MyDbContext() : base("DefaultConnection")
{ . . . }

public MyDbContext(string connectionString) : base(connectionString)
{ . . . }

My code makes use of the parameterless constructor which gets the connection string from the web config. There are some places where I instantiate a new MyDbContext object, which prohibits a solution using injection.

The route I took is to set a static property on my dbcontext with a connection string locator:

public interface IConnectionStringLocator
{ string Get(); }

public class DefaultConnectionStringLocator : IConnectionStringLocator
{
    public string Get()
    {
        return "DefaultConnection";
    }
}

public static IConnectionStringLocator ConnectionStringLocator { get; set; } =
    new DefaultConnectionStringLocator();

My web api project has the nuget packages for retrieving the key vault secrets. So in my Global.asax file I have this:

protected void Application_Start()
{
    MyDbContext.ConnectionStringLocator = new ConnectionStringLocator("DefaultConnection");
}

public class ConnectionStringLocator : IConnectionStringLocator
{
    private  readonly string _connectionStringName;

    public ConnectionStringLocator(string connectionStringName)
    {
        this._connectionStringName = connectionStringName;
    }
    public string Get()
    {
        var keyVaultName = WebConfigurationManager.AppSettings.Get("KeyVaultName");
        if (keyVaultName == "develop")
            return _connectionStringName;
        else
        {
            AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
            var keyVaultClient =
                new KeyVaultClient(
                    new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
            var defaultConnectionSecret =
                keyVaultClient.GetSecretAsync($"https://{keyVaultName}.vault.azure.net/secrets/{this._connectionStringName}");

            return defaultConnectionSecret.Result.Value;
        }
    }
}

I published this and it works, but it doesn't "feel" right.

Another option would be to follow this article https://blog.falafel.com/keeping-secrets-with-azure-key-vault/ but it would require me to couple the KeyVault API packages with my data access.

I'm looking for feedback and direction. I should add that the reason I want to use key vault is because it will allow me to have azure administrators who can view the application settings online without having access to the sql database via connection string.

KeyVault resource with the new MSI implementation: https://github.com/Azure-Samples/app-service-msi-keyvault-dotnet/

2

2 Answers

4
votes

Here's how i resolved this, in case anyone else stumbles on it.

Created a ConfigurationManager class which first tries to get the value from the key vault, but on failure it uses WebConfigurationManager to read the app settings.

    public static class ConfigurationManager
{
    public static string KeyVaultName => WebConfigurationManager.AppSettings.Get("KeyVaultName");
    private static readonly Dictionary<string, string> ConfigurationCache = new Dictionary<string, string>();
    private static SecretBundle GetSecret(string secretName, string vaultName = null)
    {
        AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
        var keyVaultClient =
            new KeyVaultClient(
                new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
        var secretUri = $"https://{vaultName ?? KeyVaultName}.vault.azure.net/secrets/{secretName}";
        var secret = keyVaultClient.GetSecretAsync(secretUri);
        return secret.Result;
    }

    public static string GetAppSettingValue(string secretName, string vaultName = null)
    {
        vaultName = vaultName ?? KeyVaultName;
        string key = $"{vaultName}:{secretName}";
        string value;

        if (ConfigurationCache.TryGetValue(key, out value))
            return value;

        if (string.IsNullOrEmpty(vaultName) || vaultName == "develop")
        {
            value = WebConfigurationManager.AppSettings.Get(secretName);
            ConfigurationCache.Add(key, value);
            return value;
        }

        var secret = GetSecret(secretName);
        value = secret.Value;
        ConfigurationCache.Add(key, value);
        return value;
    }

    public static void SetAppSettingValue(string secretName, string value, string vaultName = null)
    {
        vaultName = vaultName ?? KeyVaultName;
        string key = $"{vaultName}:{secretName}";

        if (ConfigurationCache.ContainsKey(key))
            ConfigurationCache[key] = value;
        else
        {
            WebConfigurationManager.AppSettings[key] = value;
            ConfigurationCache.Add(key, value);
        }


    }
    public static string GetConnectionStringValue(string secretName, string vaultName = null)
    {
        vaultName = vaultName ?? KeyVaultName;
        string key = $"{vaultName}:{secretName}";
        string value;

        if (ConfigurationCache.TryGetValue(key, out value))
            return value;

        if (string.IsNullOrEmpty(vaultName) || vaultName == "develop")
        {
            value = WebConfigurationManager.ConnectionStrings[secretName].ConnectionString;
            ConfigurationCache.Add(key, value);
            return value;
        }

        var secret = GetSecret(secretName);
        value = secret.Value;
        ConfigurationCache.Add(key, value);
        return value;
    }
}

Then in my dbcontext class i call on Configurationmanager.GetConnectionStringValue("DefaultConnection").

    public MyDbContext()
        : base(ConfigurationManager.GetConnectionStringValue("DefaultConnection"))
    {...}
0
votes

For .NET 4.7.1 or above, you can use Configuration Builders for .NET to connect to key vault with just a small amount of XML in appsettings or web.config files. I have used the library along with XML transformations and azure devops to connect to different key vaults based on the environment. I talked about it in my blog post.