Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF Core Connection to Azure SQL with Managed Identity

I am using EF Core to connect to a Azure SQL Database deployed to Azure App Services. I am using an access token (obtained via the Managed Identities) to connect to Azure SQL database.

Here is how I am doing that:

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    //code ignored for simplicity
    services.AddDbContext<MyCustomDBContext>();

    services.AddTransient<IDBAuthTokenService, AzureSqlAuthTokenService>();
}

MyCustomDBContext.cs

public partial class MyCustomDBContext : DbContext
{
    public IConfiguration Configuration { get; }
    public IDBAuthTokenService authTokenService { get; set; }

    public CortexContext(IConfiguration configuration, IDBAuthTokenService tokenService, DbContextOptions<MyCustomDBContext> options)
        : base(options)
    {
        Configuration = configuration;
        authTokenService = tokenService;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        SqlConnection connection = new SqlConnection();
        connection.ConnectionString = Configuration.GetConnectionString("defaultConnection");
        connection.AccessToken = authTokenService.GetToken().Result;

        optionsBuilder.UseSqlServer(connection);
    }
}

AzureSqlAuthTokenService.cs

public class AzureSqlAuthTokenService : IDBAuthTokenService
{
    public async Task<string> GetToken()
    {
        AzureServiceTokenProvider provider = new AzureServiceTokenProvider();
        var token = await provider.GetAccessTokenAsync("https://database.windows.net/");

        return token;
    }
}

This works fine and I can get data from the database. But I am not sure if this is the right way to do it.

My questions:

  1. Is this a right way to do it or will it have issues with performance?
  2. Do I need to worry about token expiration? I am not caching the token as of now.
  3. Does EF Core has any better way to handle this?
like image 884
user1868744 Avatar asked Jan 14 '19 18:01

user1868744


People also ask

Does Azure SQL support managed identity?

Azure SQL natively supports Azure AD authentication, so it can directly accept access tokens obtained using managed identities for Azure resources. You use the access token method of creating a connection to SQL.

How do I connect to an Azure managed SQL instance?

Connect with SSMSOn the on-premises client computer, open SQL Server Management Studio. In the Connect to Server dialog box, enter the fully qualified host name for your managed instance in the Server name box. Select SQL Server Authentication, provide your username and password, and then select Connect.

What is the difference between service principal and managed identity?

The key difference between Azure service principals and managed identities is that, with the latter, admins do not have to manage credentials, including passwords. To create a managed identity, go the Azure portal and navigate to the managed identity blade. Then, assign a role to the identity.


3 Answers

Is this a right way to do it or will it have issues with performance?

That is the right way. OnConfiguring is called for each new DbContext, so assuming you don't have any long-lived DbContext instances, this is the right pattern.

Do I need to worry about token expiration? I am not caching the token as of now.

AzureServiceTokenProvider takes care of caching.

Does EF Core has any better way to handle this?

The AAD Auth methods for SqlClient in .NET Core are documented here.

like image 62
David Browne - Microsoft Avatar answered Oct 15 '22 13:10

David Browne - Microsoft


While the approach is generally correct in the sense that there is no other way than having to write custom code that sets the AccessToken of the connection, there is a couple of issues in your implementation that could be avoided by using a DbConnectionInterceptor as I will describe below. Those two issues are:

  1. You took the responsibility of creating the connection object yourself. But you don't dispose it. Disposal will be tricky in your implementation, and that's why you might have skipped it.
  2. Your code is blocking, as you use .Result to block while waiting for the access token.

A better alternative is to use interceptors, which EF Core supports. You will start with a DbContext like this:

public class MyCustomDbContextFactory : IMyCustomDbContextFactory
{
    private readonly string _connectionString;
    private readonly AzureAuthenticationInterceptor _azureAuthenticationInterceptor;
    public MyCustomDbContextFactory(DbContextFactoryOptions options, AzureAuthenticationInterceptor azureAuthenticationInterceptor)
    {
        _connectionString = options.ConnectionString;
        _azureAuthenticationInterceptor = azureAuthenticationInterceptor;
    }
    public MyCustomDbContext Create()
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyCustomDbContext>();
        optionsBuilder
            .UseSqlServer(_connectionString)
            .AddInterceptors(_azureAuthenticationInterceptor);
        return new MyCustomDbContext(optionsBuilder.Options);
    }
}

And this is the interceptor implementation:

public class AzureAuthenticationInterceptor : DbConnectionInterceptor
{
    private const string AzureDatabaseResourceIdentifier = "https://database.windows.net";
    private readonly AzureServiceTokenProvider _azureServiceTokenProvider;
    public AzureAuthenticationInterceptor(AzureServiceTokenProvider azureServiceTokenProvider) : base()
    {
        _azureServiceTokenProvider = azureServiceTokenProvider;
    }
    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
    {
        if (connection is SqlConnection sqlConnection)
        {
            sqlConnection.AccessToken = await GetAccessToken();
        }
        return result;
    }
    public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
    {
        if (connection is SqlConnection sqlConnection)
        {
            sqlConnection.AccessToken = GetAccessToken().Result;
        }
        return result;
    }
    private Task<string> GetAccessToken() => _azureServiceTokenProvider.GetAccessTokenAsync(AzureDatabaseResourceIdentifier);
}

And this is how to configure your services:

services.AddSingleton(new DbContextFactoryOptions(connection_string));
services.AddSingleton(new AzureAuthenticationInterceptor(new AzureServiceTokenProvider()));

And finally, this is how to instantiate DbContext objects in your repository:

public async Task<IEnumerable<MyCustomEntity>> GetAll()
{
using var context = _notificationsDbContextFactory.Create();  // Injected in ctor
var dbos = await context.MyCustomEntity.ToListAsync();
return ... // something;
}
like image 41
romar Avatar answered Oct 15 '22 13:10

romar


For those who still fall on the same problem, I've solved the problem by using a DbInterceptor so I can asynchronously get the token without blocking the application. I had opened an issue on EF Core repo but I've closed with the solution:

https://github.com/dotnet/efcore/issues/21043

I hope it help.

like image 21
Gutemberg Ribeiro Avatar answered Oct 15 '22 13:10

Gutemberg Ribeiro