Upvoted.
This is an add-on answer to Romar's excellent answer. This was extremely useful to us and allowed us to eliminate user credentials within the ConnectionString. However, this left us with the problem of needing to retrieve the Access Token using a secret, which is sensitive information that we also do not want to include in the appsettings file. Consequently, we traded one problem for another.
There are other posts on the web that deal with this issue. So, I am posting a combined and comprehensive answer that completely removes sensitive data from the appsettings file. Note: you need to migrate the secret into the KeyVault. In this case, we named it AzureSqlSecret
. This is in order to retrieve the database user's credentials.
The Entities class constructor that calls the AzureAuthenticationInterceptor
is as follows:
public ProjectNameEntities() :
base(new DbContextOptionsBuilder<ProjectNameEntities>()
.UseSqlServer(ConfigurationManager.ConnectionStrings["ProjectNameEntities"].ConnectionString)
.AddInterceptors(new AzureAuthenticationInterceptor())
.Options)
{ }
AzureAuthenticationInterceptor:
#region NameSpaces
using Azure.Core;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Configuration;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
#endregion
namespace <ProjectName>.DataAccess.Helpers
{
public class AzureAuthenticationInterceptor : DbConnectionInterceptor
{
#region Constructor
public AzureAuthenticationInterceptor()
{
SecretClientOptions objSecretClientOptions;
string strAzureKeyVaultResourceIdentifier;
string strAzureKeyVault;
string strAzureKeyVaultUri;
strAzureKeyVaultResourceIdentifier = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:KeyVault"];
strAzureKeyVault = ConfigurationManager.AppSettings["Azure:KeyVaults:TaxPaymentSystem"];
strAzureKeyVaultUri = strAzureKeyVaultResourceIdentifier.Replace("{0}", strAzureKeyVault);
// Set the options on the SecretClient. These are default values that are recommended by Microsoft.
objSecretClientOptions = new SecretClientOptions()
{
Retry =
{
Delay= TimeSpan.FromSeconds(2),
MaxDelay = TimeSpan.FromSeconds(16),
MaxRetries = 5,
Mode = RetryMode.Exponential
}
};
this.SecretClient = new SecretClient(
vaultUri: new Uri(strAzureKeyVaultUri),
credential: new DefaultAzureCredential(),
objSecretClientOptions
);
this.KeyVaultSecret = this.SecretClient.GetSecret("AzureSqlSecret");
this.strKeyVaultSecret = this.KeyVaultSecret.Value;
this.strAzureResourceIdentifierAuthentication = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:Authentication"];
this.strAzureResourceIdentifierDatabase = ConfigurationManager.AppSettings["Azure:ResourceIdentifiers:DataBase"];
this.strClientId = ConfigurationManager.AppSettings["Azure:DatabaseUsername:ClientId"];
this.strTenantId = ConfigurationManager.AppSettings["Azure:TenantId"];
}
#endregion
#region Methods
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection objDbConnection,
ConnectionEventData objEventData,
InterceptionResult objReturn,
CancellationToken objCancellationToken = default)
{
_ILogger.Debug("Reached the Async Interceptor method");
if (objDbConnection is SqlConnection objSqlConnection)
{
objSqlConnection.AccessToken = GetAccessToken();
}
return objReturn;
}
public override InterceptionResult ConnectionOpening(
DbConnection objDbConnection,
ConnectionEventData objConnectionEventData,
InterceptionResult objReturn)
{
_ILogger.Debug("Reached the non-Async Interceptor method");
if (objDbConnection is SqlConnection objSqlConnection)
{
objSqlConnection.AccessToken = GetAccessToken();
}
return objReturn;
}
private string GetAccessToken()
{
AuthenticationContext objAuthenticationContext;
AuthenticationResult objAuthenticationResult;
ClientCredential objClientCredential;
objAuthenticationContext = new AuthenticationContext(string.Format("{0}/{1}"
, this.strAzureResourceIdentifierAuthentication
, this.strTenantId));
objClientCredential = new ClientCredential(this.strClientId, this.strKeyVaultSecret);
objAuthenticationResult = objAuthenticationContext.AcquireTokenAsync(this.strAzureResourceIdentifierDatabase, objClientCredential).Result;
return objAuthenticationResult.AccessToken;
}
#endregion
#region Properties
readonly <ProjectName>.Common.Logging.ILogger _ILogger = <ProjectName>.Common.Logging.LogWrapper.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private SecretClient SecretClient;
private KeyVaultSecret KeyVaultSecret;
private string strAzureResourceIdentifierDatabase;
private string strAzureResourceIdentifierAuthentication;
private string strKeyVaultSecret;
private string strClientId;
private string strTenantId;
#endregion
}
}