Azure Function To Azure Function Request using DefaultAzureCredential and HttpClient

Viewed 1377

I need to call a Http Azure Function from another Azure Function.

At present, I call an Azure Key Vault to get the target Function's Key, and put that in the URL as documented here: https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=csharp#api-key-authorization

However, I want to start using a Managed Identity and DefaultAzureCredential but I cannot find out how to use DefaultAzureCredential with HttpClient or similar.

How could I use DefaultAzureCredential and HttpClient to call a Function from another Function?

2 Answers

The simplistic way of solving this issue is like this:

var targetFunctionAppAppRegistrationApplicationId = "A Guid that you must get from your target Function's Authentication configuration - 'App (client) ID'";
var url = "https://yourfunctionappname.azurewebsites.net/api/targetfunctionname";
var creds = new DefaultAzureCredential();
var token = await creds.GetTokenAsync(new Azure.Core.TokenRequestContext(new[] { targetFunctionAppAppRegistrationApplicationId }));
using (HttpClient client = new HttpClient())
{
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
  var result = await client.GetAsync(url);
  // Anything else you want to do with the result
}

Credits for the above to https://spblog.net/post/2021/09/28/call-azure-ad-secured-azure-function-from-logic-app-or-another-function-with-managed-identity

However

The code above will soon cause socket exhaustion. The correct way is to use HttpClientFactory, as explained here: https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

Since this specific use case is not covered in those docs, below is an example of how it would look like.

First, you need a MessageHandler:

public class AzureDefaultCredentialsAuthorizationMessageHandler : DelegatingHandler
{
  private readonly TokenRequestContext TokenRequestContext;
  private readonly DefaultAzureCredential Credentials;

  public AzureDefaultCredentialsAuthorizationMessageHandler()
  {
    // This parameter is actually a list of scopes.
    // If your target Function has defined scopes then you should use them here.
    // TokenRequestContext also supports many other options you should probably check out.
    TokenRequestContext = new (new[] { "targetFunctionAppAppRegistrationApplicationId" }); 
    Credentials = new DefaultAzureCredential();
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var tokenResult = await Credentials.GetTokenAsync(TokenRequestContext, cancellationToken);
    var authorizationHeader = new AuthenticationHeaderValue("Bearer", tokenResult.Token);
    request.Headers.Authorization = authorizationHeader;
    return await base.SendAsync(request, cancellationToken);
  }
}

You then need to register an HttpClient with this message handler in your Dependency Injection container. If you're using the standard IServiceCollection:

services
  .AddScoped<AzureDefaultCredentialsAuthorizationMessageHandler>()
  .AddHttpClient<YourClassUsingTheHttpClient>((serviceProvider, httpClient) => 
  {
    httpClient.BaseAddress = "https://yourfunctionappname.azurewebsites.net/api/targetfunctionname";
  }).AddHttpMessageHandler<AzureDefaultCredentialsAuthorizationMessageHandler>();

Finally, just have a YourClassUsingTheHttpClient class that takes an HttpClient in its constructor:

public class YourClassUsingTheHttpClient
{
  public YourClassUsingTheHttpClient(HttpClient httpClient) { ... }
}

Notes

It should be noted that the code above does not deal with other important concerns like:

  1. Error handling
  2. Token caching
  3. Ability to have different HttpClients and MessageHandlers for different API endpoints.

Error handler should be straightforward to add. The rest go beyond the scope of this question.

Prerequisites to use DefaultAzureCredentials in Azure Function with Http Client/Request

  1. Azure CLI
  2. Azure Function Core Tools
  3. .Net Core 3.1 SDK
  4. Terraform CLI

The following steps used to create and deploy the resources to Azure:

1. Deploy resources to Azure:

 az login
az account set --subscription < target subscription ID>
cd terraform
terraform apply --var basename="< resourcename>" --var resource_group_name="<resource group name>" --var location="<Azure region name>"

2. Deploy Function App To Azure:

It asks you if you have multiple Functions core tools versions installed (like v1, v2, v3). After deployment completes, a deploy.app.sh file will be created and executed within a bash shell, which fully deploys the Function App to Azure.

3. Observe the config of the Function App

Observe the connection strings under Configuration Menu of the Function App in the Portal which contains the associated storage account connection string, where this storage account contains the uploaded .zip package of Functions File Content when it's published.

4. Rolling the Keys in storage account using Managed Identity

Under the Function App in Azure portal > Identity > System Assigned (Switch the status to ON) and click on Save.

No need to restart the function app to start using the new keys from the storage account.

Execute the below command to generate a SAS URL:

curl --location --request GET 'https://fxnxxxxxx.azurewebsites.net/api/GetSASUrl?code=3TR6xxxxxx&blobUri=https://fxxxx.blob.core.windows.net/sample/my.file'

Here the blobUri is the full Http URL to your target blob.

To download the blob, hit this URL in an InPrivate Browser:

https://fxn_____.blob.core.windows.net/sample/my.file?skoid=......pxLSpVwuML%2B3UXrxBmC6XGA%3D

Below Command gets the storage account keys:

curl --location --request GET 'https://fxnxxxxxxx.azurewebsites.net/api/GetAccountKeys?code=GKUxxxxxxxx&accountName=fxnxxxx`

accountName is the Storage account name which is equivalent to the basename variable passed in above Terraform command.

  • The Response comes in JSON Format contains of key-value pairs related to keyName, value and permissions.

As Part of normal security protocol, regenerate the storage account keys by going to Azure Portal > Storage account > Access Keys > regenerate.

Check the change in keys using this command:

curl --location --request POST 'https://fxnxxxxxx.azurewebsites.net/api/RegenerateKey?code=9OZxxxxx&accountName=fxnstormsisampsc&keyName=key2`

The response will be in Http Status Code 200 OK and check in Azure Portal > Storage account > Access Keys.

Here is the reference code and documentation.

Related