[QUERY] Token is expired using Identity with SQL
See original GitHub issueLibrary name and version
Azure.Identity 1.8
Query/Question
I am trying to update an Azure app service to use User-assigned managed identity to access SQL but am running into the following error: The underlying provider failed on Open. Login failed for user '<token-identified principal>'. Token is expired.
Appreciate any ideas on what should be done differently to solve this issue?
Details: I have a Class Library targeting .Net Framework 4.7, using Entity Framework 6.4 to connect to Azure SQL (database first). Class library references System.Data.SqlClient, Azure.Core 1.28 and Azure.Identity 1.8.
I have modified the DbContext class to use a supported connection string (see below) and use a static DefaultAzureCredential
class to fetch a token from Azure AD and assign it to the connection object. When a Web API application using the class library is deployed to Azure App service, everything works as expected for about 24 hours, then the following errors starts popping up:
**The underlying provider failed on Open.
Login failed for user '<token-identified principal>'. Token is expired.**
These errors go away if the app is restarted and it works fine for another 24 hours before needing a restart. Azure AD managed identity tokens have a lifetime of about 24 hours. It looks like when the token expires, the connections in the pool retain the old token and fail upon subsequent use. Should the SDK handle refreshing the tokens for the connections in the pool?
I have also tried the following without success:
- Declare DefaultAzureCredential as a non-static class member - Causes every request to get a new instance of DefaultAzureCredential. Seeing degraded performance in the database calls, because DefaultAzureCredential attempts to use all the possible different ways to login until it finds one that works.
- Declare DefaultAzureCredential as non-static but made it a scoped instance, per request - same issue as in number 1.
- Declare AccessToken as a static class member and handle token refresh based on the expiry - Same error as original. SQL Connections retain the expired token which leads to login failed errors.
- Automatically retry any commands that fail due to connection breaks by using Entity Framework’s SqlAzureExecutionStrategy (SQL Error: 18456) - Retries do happen but without first fetching a new token from Azure AD.
Here is what the code in the class library looks like:
public partial class TestDbEntities
{
private static volatile DefaultAzureCredential _defaultAzureCredential = null;
private static object _syncObject = new object();
private static readonly Regex connectionRegex = new Regex(@"provider\s*connection\s*string\s*=\s*""(?<conn>[^""]+)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public TestDbEntities()
: base("name=TestDbEntities")
{
DefaultAzureCredential defaultCredentialInstance = GetDefaultCredentialInstance();
var conn = (SqlConnection)Database.Connection;
var connectionString = <<get connection string from config>>
conn.ConnectionString = connectionRegex.Match(connectionString).Groups["conn"].ToString();
AccessToken accessToken = defaultCredentialInstance.GetToken(new TokenRequestContext(new[] { "https://database.windows.net/.default" }));
conn.AccessToken = accessToken.Token;
}
private DefaultAzureCredential GetDefaultCredentialInstance()
{
if (_defaultAzureCredential == null)
{
lock (_syncObject)
{
if (_defaultAzureCredential is null)
{
DefaultAzureCredentialOptions defaultAzureCredentialOptions = new DefaultAzureCredentialOptions
{
ExcludeAzureCliCredential = true,
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeInteractiveBrowserCredential = true,
ExcludeManagedIdentityCredential = false,
ExcludeVisualStudioCodeCredential = true,
ExcludeVisualStudioCredential = true,
ManagedIdentityClientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")
};
_defaultAzureCredential = new DefaultAzureCredential(defaultAzureCredentialOptions);
}
}
}
return _defaultAzureCredential;
}
}
EF connection string looks like:
<add name="TestDbEntities" connectionString="metadata=res://*/Entity.TestDbModel.csdl|res://*/Entity.TestDbModel.ssdl|res://*/Entity.TestDbModel.msl;provider=System.Data.SqlClient;provider connection string="server=tcp:xxxx.database.windows.net,1433;database=Test;persist security info=True;MultipleActiveResultSets=True;App=TestMI;"" providerName="System.Data.EntityClient" />
Environment
No response
Issue Analytics
- State:
- Created 5 months ago
- Comments:11 (4 by maintainers)
Top GitHub Comments
Yes, Mirosoft.Data.SqlClient (MDS) has a lot of improvements around Azure AD auth and token handling. It even has authentication=Active Directory Default, which uses DefaultAzureCredential underneath, so you don’t have to do your own token acquisition. However, even in MDS, just setting the AccessToken property still has limitations since it doesn’t include expiration info. We are considering additional token authentication options in the future.
If you aren’t ready to move to MDS, you can override the implementation of the “Active Directory Interactive” authentication provider with your own class that implements “SqlAuthenticationProvider” and register it in your application by calling the “SetProvider” API. With that method, SDS knows the expiration date and should recognize that a pooled connection has an expired token and remove it from the pool. Custom providers are still supported in
Code sample (MDS-specific, but still applies to SDS if you pick an authentication method from SDS): https://github.com/dotnet/SqlClient/blob/main/doc/samples/CustomDeviceCodeFlowAzureAuthenticationProvider.cs
Thanks @David-Engel for providing additional context.
DefaultAzureCredential is responsible for getting a new token that typically expires 24 hours after the initial request. However, it does not get a new token until about 3-5 minutes before expiry (the SDK returns the same token until the previous one is about to expire).
Because new connections can be created at any time, Connection Lifetime would have to be dynamic depending on how much time was left before expiry. This will cause numerous pools to be created (one per distinct connection string). I can check how it performs.
Would have been ideal if the SqlConnection had information on the expiration date of the token. A pool could use that information to not return a connection with an expired token.
Looks like Microsoft.Data.SqlClient solves for this issue. I see a PR #635 for the same.