DB queries hang when adding password fallback mechanism to primary AAD connection
See original GitHub issueWe are currently facing issues when trying to implement fallback logic in the way we connect to our Azure SQL database from our Azure cloud services.
We currently connect to our Azure SQL DB using AAD authentication. What we are trying to do is implement fallback logic for this connection, where if there is a failure in the initial connection (ex. when acquiring the AAD access token), we fallback to a secondary connection. The secondary connection uses username + password authentication.
The issue we are facing is that on an Azure cloud service using EF6, the fallback path doesn’t work. What we get is that the connection itself is initialized successfully, but any DB call via EF hangs forever with no response. During the investigation, we have done the following:
- Trying the same on an Azure function app, it works. So it seems to only fail on Azure cloud services.
- Issue is not reproducible locally, only on a remote Azure Cloud service.
- When using username + password as primary authentication (no AAD connection anymore), things work, so it seems that it is not an issue with password authentication on Azure cloud services in general.
- Examining long running queries on our Azure SQL instance, we don’t find the queries that the cloud service indicates that are hanging. So the queries seem to never reach the DB.
Steps to reproduce
Code snippet for initial AAD logic we had:
public static SqlConnection InitializeSqlConnectionWithAad(
string dbConnectionString,
string dbClientIdThumbprintPair)
{
var accessToken = AsyncHelpers.RunSync(() => GetAccessToken(dbClientIdThumbprintPair));
var connection = new SqlConnection(dbConnectionString)
{
AccessToken = accessToken
};
if (connection.AccessToken != null)
{
connection.Open();
}
return connection;
}
private static async Task<string> GetAccessToken(
string dbClientIdThumbprintPair)
{
if (string.IsNullOrWhiteSpace(dbClientIdThumbprintPair))
{
return null;
}
var credentialsPair = dbClientIdThumbprintPair.Split(';');
if (credentialsPair.Length != 2)
{
Logger.LogError("Invalid Client ID Thumbprint pair provided for database connection");
throw new Exception("Invalid Client ID Thumbprint pair provided for database connection");
}
var clientId = credentialsPair[0];
var clientThumbprint = credentialsPair[1];
var cert = CertificateHelper.FindCertificateByThumbprint(clientThumbprint);
if (cert == null)
{
Logger.LogError("Certificate not found for database connection");
throw new Exception("Certificate not found for database connection");
}
var clientCertificate = new ClientAssertionCertificate(clientId, cert);
string aadInstance = ConfigAccessor.GetSetting("OAuthAADInstance");
string tenant = ConfigAccessor.GetSetting("OAuthAADTenant");
string authority = string.Format(System.Globalization.CultureInfo.InvariantCulture, aadInstance, tenant);
string dbAppURI = ConfigAccessor.GetSetting("DBResourceURI");
var authContext = new AuthenticationContext(authority);
var authenticationResult = await authContext.AcquireTokenAsync(
dbAppURI,
clientCertificate,
sendX5c: true).ConfigureAwait(false);
return authenticationResult.AccessToken;
}
Code snippet for fallback logic (When an exception is thrown from AAD path, the fallback part with password is the path having issues):
public static SqlConnection InitializeSqlConnection(
string dbConnectionString,
string dbClientIdThumbprintPair,
string dbConnStrWithPassword)
{
SqlConnection connection = null;
bool aadConnectionSucceeded;
try
{
connection = InitializeSqlConnectionWithAad(dbConnectionString, dbClientIdThumbprintPair);
aadConnectionSucceeded = true;
}
catch (Exception ex)
{
Logger.LogError("Sql AAD connection failed. Falling back to secondary connection with password.", ex.ToString());
aadConnectionSucceeded = false;
}
if (!aadConnectionSucceeded)
{
try
{
connection = new SqlConnection(dbConnStrWithPassword);
}
catch (Exception ex)
{
Logger.LogError("Sql password fallback connection failed.", ex.ToString());
throw;
}
}
return connection;
}
Further technical details
EF version: 6.3 Target framework: Net Framework 4.6.2 Operating system: Windows Server 2016 IDE: Not reproducible locally
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (3 by maintainers)
@ajcvickers turned out to be an issue in our client code that we fixed, so we can close this issue.
For future reference in case anyone faces a similar issue, the problem was in the implementation of AsyncHelpers.RunSync which is provided in many online resources. The original implementation is as follows:
Now the issue with the above is that the synchronization context was only set back to the old context only if no exception was thrown. If an exception was thrown, this doesn’t happen which risks deadlocks, which happened in our case. Now to solve the issue, we simply moved the restoring of the synchronization context to be inside a
finally
statement:@AmirGeorge There doesn’t seem anything directly related to EF here–it’s all code that uses SqlClient only. Is there a specific reason that you filed this as an EF issue?