Using Azure Identity with Azure SQL, Graph, and Entity Framework

Avatar

Mickaƫl

Hi there šŸ‘‹

My name is MickaĆ«l Derriey and I work at Telstra Purple, the largest IT consultancy in Australia. I’m part of an internal team where my main focus is to support .NET applications we developed in-house, most of which are hosted in Azure and integrate with a variety of workloads like Azure SQL, Blob Storage, or the Microsoft Graph API.

We’re always on the lookout to improve our security posture. One aspect of this is making sure we properly secure sensitive information, like connection strings, API keys, and the secrets associated with our Azure Active Directory apps. Azure Managed Identities is a feature that provides the application host, like an App Service or Azure Functions instance, an identity of its own which can be used to authenticate to services that support Azure Active Directory without any credentials stored in the code or the application configuration.

We found that Azure Identity helps us leverage that capability as it abstracts away the specifics of the token acquisition process when working with Managed Identities. It also implements support for a variety of credentials sources while exposing a consistent and easy-to-use API.

We wanted to share our experience leveraging Azure Identity, how it allows us to free our applications from credentials when deployed on Azure while providing a nice development time experience.

What is Azure Identity

The Azure Identity library is a token acquisition solution for Azure Active Directory.

The main strength of Azure Identity is that it’s integrated with all the new Azure SDK client libraries that support Azure Active Directory authentication, and provides a consistent authentication API. See the Azure SDK Releases page for a full list of the client libraries that support Azure Identity.

Another benefit of Azure Identity is the fact it sources credentials from a variety of places, while abstracting away the specificities of each credential. For example, at the time of writing, the often used DefaultAzureCredential class will try to use the following credentials to acquire a token:

  • Application credentials coming from environment variables;
  • The Azure Managed Identity associated with the Azure host the application is running on;
  • The account that a developer is signed in to in Visual Studio;
  • The account the developer has logged in to in the “Azure Account” Visual Studio Code extension; and finally
  • The account the developer has logged in to the Azure CLI.

This means that the same code can handle AAD authentication at development time, as well as when the solution is deployed to Azure, while accounting for the differences in the token acquisition process. For example, the application credentials coming from environment variables will be used to perform a standard OAuth 2.0 client credentials flow. However, if the Managed Identity credentials are used, it will issue a request to the identity endpoint instead, all transparently to the consumer of the library.

Let’s now see which credentials we use in our internal applications.

Which credentials we use

We mentioned before that the DefaultAzureCredential can get credentials from a variety of sources that suit both development time scenarios as well as when our application is deployed to Azure.

When we work on internal applications at Telstra Purple, at development time we often use local resources. This means our apps connect to a local SQL Server database or Azurite, a cross-platform Azure Storage emulator. As a result, most of the time we only leverage Azure Active Directory authentication when the applications are deployed in Azure. Every now and then, though, we want to use AAD authentication locally to ensure that it’s behaving as expected.

As mentioned before, Azure Identity has native support for development time as it can use the credentials of the accounts that developers have logged in to Visual Studio, VS Code, or the Azure CLI. While we might look into using those in the future, we’re currently sharing the client secret of the development AAD app registration within the team with the help of a password manager. Luckily, Azure Identity exposes a ChainedTokenCredential class that allows us to define exactly which credentials sources we want to use.

var credential = new ChainedTokenCredential(
    new ManagedIdentityCredential(),
    new EnvironmentCredential());

This ensures that the library will only try to authenticate to external services using the Managed Identity credentials, or the ones from environment variables.

Our applications leverage Azure Managed Identity as much as possible as it allows us not to have to manage sensitive credentials whatsoever, like AAD client secrets. However, the Managed Identity context is only available when the application is deployed to Azure, and there is no way to emulate it locally. As a result, we add the environment credential to the list as well, which allows us to enable AAD authentication at development time.

How we work around environment variables

Most of applications are built with ASP.NET Core, so when we want to test AAD authentication locally, one way to set environment variables is to use the launchSettings.json file:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "Api": {
      "commandName": "Project",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "AZURE_TENANT_ID": "<azure-tenant-id>",
        "AZURE_CLIENT_ID": "<aad-app-client-id>",
        "AZURE_CLIENT_SECRET": "<aad-app-client-secret>"
      },
      "applicationUrl": "https://localhost:44300"
    }
  }
}

The three variables prefixed with AZURE_ are the ones the EnvironmentCredential class will look for, so this allows us to “light up” AAD authentication easily.

However, the launchSettings.json file is usually committed to source control, so there’s a possibility that we mistakenly commit sensitive information, which is never a good thing. We’ve become accustomed to leveraging the ASP.NET Core configuration system, which supports specifying multiple providers of configuration data. For secrets, we usually use the ASP.NET Core Secret Manager which stores data in JSON files outside of the Git repository, making sure nothing sensitive gets committed.

Thankfully for us, when it detects the presence of a client secret, the EnvironmentCredential class internally uses the ClientSecretCredential class, which itself defines a constructor that doesn’t depend on environment variables, but accepts string parameters for the tenant id, client id, and client secret.

In the end, we leverage Azure Identity so it abstracts away the token acquisition process, and stitches it together with the ASP.NET Core configuration system, which is not only more familiar to our team, but also more secure as it prevents us from committing secrets to source control. The configuration could look like this.

First, we define a new section in our appsettings.json file to hold the tenant id, client id, and client secret:

{
    "AzureAdAuthenticationOptions": {
        "TenantId": "<azure-tenant-id>",
        "ClientId": "<aad-app-client-id>",
        "ClientSecret": "set in user secrets"
    }
}

Developers would then use the Secret Manager to store the client secret:

dotnet user-secrets set 'AzureAdAuthenticationOptions:ClientSecret' '<their-personal-client-secret>'

The code base would define a custom class matching the configuration section:

public class AzureAdAuthenticationOptions
{
    public string TenantId { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
}

The code setting up the Azure Identity credential would then leverage the IConfiguration service:

public class TokenCredentialProvider
{
    private readonly IConfiguration _configuration;

    public TokenCredentialProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public TokenCredential GetCredential()
    {
        // Bind the configuration section to an instance of AzureAdAuthenticationOptions
        var azureAdOptions = _configuration.GetSection(nameof(AzureAdAuthenticationOptions)).Get<AzureAdAuthenticationOptions>();

        // If all three values are present, use both the Managed Identity and client secret credentials
        if (!string.IsNullOrEmpty(azureAdOptions.TenantId) &&
            !string.IsNullOrEmpty(azureAdOptions.ClientId) &&
            !string.IsNullOrEmpty(azureAdOptions.ClientSecret))
        {
            return new ChainedTokenCredential(
                new ManagedIdentityCredential(),
                new ClientSecretCredential(
                    azureAdOptions.TenantId,
                    azureAdOptions.ClientId,
                    azureAdOptions.ClientSecret));
        }

        // Otherwise, only use the Managed Identity credential
        return new ManagedIdentityCredential();
    }
}

This solution requires an additional step compared to when we were using EnvironmentCredential. We need to check that the three values are present as ClientSecretCredential requires all of them. We think it’s a small trade-off to get the flexibility of the ASP.NET Core configuration system, along with the peace of mind that secrets won’t be committed to source control.

For brevity, the remainder of this post will use the EnvironmentCredential class, provided out of the box.

Next, we’ll discuss how we decide whether to use Azure Active Directory authentication when connnecting to different services.

Blob Storage integration

We previously pointed out that we often use local services at development time, such as Azurite. In such cases, there’s no need for Azure Identity to take care of AAD authentication. However, when deployed to Azure, we need it to, so we must detect whether to enable it.

The Azure Blob Storage client library for .NET needs to be given the URL of the storage account blob endpoint, as shown in the README on GitHub. The configuration for Azure Blob Storage can then either be:

  • The special development connection string, UseDevelopmentStorage=true, recognised by Azurite;
  • A fully-fledged connection string the storage account, like DefaultEndpointsProtocol=https;AccountName=<account-name>;AccountKey=<account-key>; or finally
  • The URL to the storage account blob endpoint, such as https://<account-name>.blob.core.windows.net.

Since only the last of these needs to use AAD authentication, our current strategy is to try and parse the “connection string” into a URI. If the parse operation fails, we use the connection string as-is, assuming that it contains the credentials required.

public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration.GetConnectionString("AzureStorageAccount");

    services.AddAzureClients(builder =>
    {
        if (Uri.TryCreate(connectionString, UriKind.Absolute, out var storageAccountUri))
        {
            var tokenCredential = new ChainedTokenCredential(
                new ManagedIdentityCredential(),
                new EnvironmentCredential());

            builder
                .AddBlobServiceClient(storageAccountUri)
                .WithCredential(tokenCredential);
        }
        else
        {
            builder.AddBlobServiceClient(connectionString);
        }
    });
}

The above sample uses the Microsoft.Extensions.Azure NuGet package which provides extension methods that help with the registration of Azure clients in the built-in ASP.NET Core dependency injection container. However, the logic used to detect whether we want to use AAD authentication is not dependent on this package and could be used in a scenario where the BlobServiceClient instance is manually created.

EF Core integration

We saw in the previous section how the Azure Identity library integrates nicely with the Azure Blob Storage client library. However, at its heart, its goal is to facilitate the token acquisition process. As such, nothing prevents us from leveraging it to acquire tokens outside of the Azure SDK for .NET. Let’s see how we use it to use AAD authentication to Azure SQL. For more information about this subject, please see the official documentation at https://docs.microsoft.com/azure/azure-sql/database/authentication-aad-overview.

Most of our apps integrate with SQL databases, either through a micro-ORM like Dapper, or a fully-fledged one like EF Core. In an effort to minimise the number of credentials we need to maintain, we try as much as we can to connect to Azure SQL databases using the Managed Identity of the Azure host our applications run on. While Azure Identity isn’t officially supported or integrated with these libraries, we need to acquire the tokens manually. Thankfully, the API is straightforward; the TokenCredential class defines two methods to acquire tokens, one synchronous, and the other one asynchronous. Here’s an extract of the implementation:

/// <summary>
/// Represents a credential capable of providing an OAuth token.
/// </summary>
public abstract class TokenCredential
{
    /// <summary>
    /// Gets an <see cref="AccessToken"/> for the specified set of scopes.
    /// </summary>
    /// <param name="requestContext">The <see cref="TokenRequestContext"/> with authentication information.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
    /// <returns>A valid <see cref="AccessToken"/>.</returns>
    public abstract ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken);

    /// <summary>
    /// Gets an <see cref="AccessToken"/> for the specified set of scopes.
    /// </summary>
    /// <param name="requestContext">The <see cref="TokenRequestContext"/> with authentication information.</param>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
    /// <returns>A valid <see cref="AccessToken"/>.</returns>
    public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken);
}

To connect to Azure SQL using AAD authentication, the Microsoft.Data.SqlClient NuGet package defines an AccessToken property on the SqlConnection class. Because EF Core manages the lifetimes of the SQL connections, we leverage the concept of interceptors, which were introduced in version 3.0. Interceptors lets us implement custom logic during specific events. The DbConnectionInterceptor class has both a synchronous ConnectionOpening and an asynchronous ConnectionOpeningAsync methods, which are the perfect fit for us to get a token and attach it to the connection. We need to override both methods, as EF Core will invoke the synchronous method during synchronous queries, and the async one for async queries. If we’re positive we only ever use synchronous or asynchronous queries, we can only override the appropriate method.

We also implemented a detection mechanism to determine whether we need AAD authentication. We found that, in our cases, two conditions are required to indicate that we want to use token-based authentication:

  1. We connect to an Azure SQL database, which we translate to “does the target server name contain database.windows.net“; and
  2. The specified connection string doesn’t define a username.

All in all, the interceptor looks like below:

public class AzureAdAuthenticationDbConnectionInterceptor : DbConnectionInterceptor
{
    // See https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/services-support-managed-identities#azure-sql
    private static readonly string[] _azureSqlScopes = new[]
    {
        "https://database.windows.net//.default"
    };

    private static readonly TokenCredential _credential = new ChainedTokenCredential(
        new ManagedIdentityCredential(),
        new EnvironmentCredential());

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
    {
        var sqlConnection = (SqlConnection)connection;
        if (DoesConnectionNeedAccessToken(sqlConnection))
        {
            var tokenRequestContext = new TokenRequestContext(_azureSqlScopes);
            var token = _credential.GetToken(tokenRequestContext, default);

            sqlConnection.AccessToken = token.Token;
        }

        return base.ConnectionOpening(connection, eventData, result);
    }

    public override async Task<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
    {
        var sqlConnection = (SqlConnection)connection;
        if (DoesConnectionNeedAccessToken(sqlConnection))
        {
            var tokenRequestContext = new TokenRequestContext(_azureSqlScopes);
            var token = await _credential.GetTokenAsync(tokenRequestContext, cancellationToken);

            sqlConnection.AccessToken = token.Token;
        }

        return await base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
    }

    private static bool DoesConnectionNeedAccessToken(SqlConnection connection)
    {
        //
        // Only try to get a token from AAD if
        //  - We connect to an Azure SQL instance; and
        //  - The connection doesn't specify a username.
        //
        var connectionStringBuilder = new SqlConnectionStringBuilder(connection.ConnectionString);

        return connectionStringBuilder.DataSource.Contains("database.windows.net", StringComparison.OrdinalIgnoreCase) && string.IsNullOrEmpty(connectionStringBuilder.UserID);
    }
}

It can then be registered within our EF Core DbContext instance:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationContext>(options =>
    {
        options.UseSqlServer("<connection-string>");
        options.AddInterceptors(new AzureAdAuthenticationDbConnectionInterceptor());
    });
}

The above setup gives our applications the ability to connect to Azure SQL by leveraging the Managed Identity of the Azure resource they are deployed to. It’s a big win for us from a security point of view, as we don’t need to worry about securing the connection string in Key Vault, for example.

Microsoft Graph API integration

Some applications rely on background jobs to perform some recurrent tasks, like synchronisation of data, or sending our reminder emails. Typically, daemon applications don’t hold a user context, so we can’t use the identity of a logged in user to integrate with other services, like the Microsoft Graph API. In such cases, we need to rely on the identity of the application, be it the Managed Identity of the host resource or the credentials of the AAD app registration.

As we’ve seen in the previous section, leveraging the token acquisition capability of Azure Identity is straightforward, so could also use it to acquire a token intended to be used against the Microsoft Graph API. If we want to call the Graph API as a Managed Identity, we need to assign application permissions to the backing AAD service principal.

While the Azure portal doesn’t currently allow us to do this, this can be done through PowerShell or the Azure CLI. For an example on how to do this, please see the great post that my colleague Rahul Nath wrote on the subject: https://www.rahulpnath.com/blog/how-to-authenticate-with-microsoft-graph-api-using-managed-service-identity. While the sample code uses a different library to get a token, the sample above should make it easy to switch to Azure Identity.

Consistent APIs across SDKs

While most of our internal applications are based on .NET, we recently started developing a new API using Apollo, a Node.js GraphQL implementation. This new project aggregates data from various sources, one of them being an Azure Blob Storage account.

It was a great surprise when we realised the APIs of the @azure/identity npm package were consistent with the ones provided by the Azure.Identity NuGet package! We found the base TokenCredential class, the default DefaultAzureCredential implementation that sources credentials from various places, and the ChainedTokenCredential one that gives us the possibility to pick which credentials we want to use.

The same was also true for the Blob Storage client libraries; the similarities between the @azure/storage-blob npm package and Azure.Storage.Blobs NuGet package means we didn’t have to familiarise ourselves with a new library.

Here’s a simplified version of the code used to configure the Blob Storage client in the Node.js app:

import { TokenCredential } from '@azure/core-http';
import { ManagedIdentityCredential } from '@azure/identity';
import { BlobServiceClient, ContainerClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import config from 'config';

export class BlobService {
    private readonly blobServiceClient: BlobServiceClient;

    constructor() {
        this.blobServiceClient = new BlobServiceClient(
            `https://${config.get('blobStorage.accountName')}.core.windows.net`,
            BlobService.getCredential(),
        );
    }

    public containerClient(containerName: string): ContainerClient {
        return this.blobServiceClient.getContainerClient(containerName);
    }

    private static getCredential(): TokenCredential | StorageSharedKeyCredential {
        if (config.has('blobStorage.key')) {
            return new StorageSharedKeyCredential(
                config.get('blobStorage.accountName'),
                config.get('blobStorage.key'),
            );
        }

        return new ManagedIdentityCredential();
    }
}

This code shares many similarities with the .NET sample we previously saw. It uses many classes which names are already familiar to us. It also implements a detection mechanism to determine whether we authenticate to the storage account with an account key or with a token acquired for us by the ManagedIdentityCredential class.

Consistent APIs in the different SDKs means we can get up and running really quick, all while leveraging the same benefits of the Azure Identity libraries.

Conclusion

In this post, we first went over what the value proposition of the Azure Identity library is, and the many sources of credentials it leverages by default. We then looked at the credentials we use at Telstra Purple, along with how we can keep using the ASP.NET Core configuration system that we rely on in many of our applications.

Next, we discussed how the Azure Blob Storage client library has native support for Azure Identity, and the detection mechanism we implement to determine whether we want to use AAD authentication, as it’s usually not the case at development time when we connect to the Azure Storage Emulator.

The next section was dedicated to how we can use Azure Identity outside of the Azure SDK for .NET to connect to Azure SQL through EF Core. This opened up the possibility of integrating with any token-based service backed by Azure Active Directory, like the Microsoft Graph API.

Finally, we stepped out of the .NET world, and gladly discovered that the JavaScript/TypeScript Azure SDKs share many similarities with their .NET counterparts, which makes for a fantastic experience as it virtually removes any learning curve and allows to leverage the same concepts across different languages.

Azure SDK Blog Contributions

Thank you for reading this Azure SDK blog post! We hope that you learned something new and welcome you to share this post. We are open to Azure SDK blog contributions. Please contact us at azsdkblog@microsoft.com with your topic and we’ll get you set up as a guest blogger.

0 comments

Leave a comment