'Azure Managed ID: how to convert azure function from connection string to managed id

I have an Azure function that looks like this:

   [FunctionName("CreateWidgetWorkspace")]
    public async Task<IActionResult> CreateWidgetWorkspace(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "widget/workspaces")] HttpRequest req,
         [Queue("widgetworkspaces"), StorageAccount("WidgetStorageQueue")] ICollector<string> messageQueue,
        ILogger log)
    {           
       
        WorkspaceResponse response = new WorkspaceResponse();
        var content = await new StreamReader(req.Body).ReadToEndAsync();
        log.LogInformation($"Received following payload: {content}");

        var workspaceRequest = JsonConvert.DeserializeObject<Workspace>(content);
        if (workspaceRequest.name != null){     
                messageQueue.Add(JsonConvert.SerializeObject(workspaceRequest));                                                 
        } 
        else {
            response.status = "Error: Invalid Request";
            response.requestId=null;
        }
        return new OkObjectResult(JsonConvert.SerializeObject(response));  
    }

Everythings works well - the connection string is defined in my local.settings.json file like this:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "WidgetStorageQueue": 

"DefaultEndpointsProtocol=https;AccountName=accountname;AccountKey=asdf+asdf+AStRrziLg==" } }

But now i've created a managed Identity and it has been assigned "Contributor" role on all resources inside the resource group. So I need to refactor this code to no longer use the connection string in local.settings / environment variables. But to use the managed id. Can you point me to an article or video that would put me on the right path? I actually prefer not to Azure key vault if possible.

Thanks.

EDIT 1

I've added the 2 packages referenced in the answer. This is what I have in my csproj file:

  <ItemGroup>
    <PackageReference Include="Azure.Data.Tables" Version="12.4.0" />
    <PackageReference Include="Azure.Storage.Queues" Version="12.10.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.Azure.Webjobs.Extensions.ServiceBus" Version="5.2.0" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage.Queues" Version="5.0.1" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </None>
  </ItemGroup>

And this is what my local.settings.json file looks like:

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "dotnet","WidgetStorageQueue__queueServiceUri":"https://mystorageaccountname.queue.core.windows.net"
  },
  "ConnectionStrings": {}
}

But I'm getting an error :

2022-05-05T19:30:00.774Z] Executed 'CreateWidgetWorkspace' (Failed, Id=asdf-a22b-asdf-asdf-asdf, Duration=6356ms)
[2022-05-05T19:30:00.777Z] System.Private.CoreLib: Exception while executing function: CreateWidgetWorkspace. Azure.Storage.Queues: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:asdf-8003-asdf-asdf-asdf
Time:2022-05-05T19:30:00.7494033Z
[2022-05-05T19:30:00.781Z] Status: 403 (Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.)
[2022-05-05T19:30:00.782Z] ErrorCode: AuthenticationFailed
[2022-05-05T19:30:00.784Z] 
[2022-05-05T19:30:00.785Z] Additional Information:
[2022-05-05T19:30:00.788Z] AuthenticationErrorDetail: Issuer validation failed. Issuer did not match.
[2022-05-05T19:30:00.790Z] 
[2022-05-05T19:30:00.791Z] Content:
[2022-05-05T19:30:00.793Z] <?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
RequestId:asdf-8003-asdf-asdf-asdf
Time:2022-05-05T19:30:00.7494033Z</Message><AuthenticationErrorDetail>Issuer validation failed. Issuer did not match.</AuthenticationErrorDetail></Error>      
[2022-05-05T19:30:00.795Z] 
[2022-05-05T19:30:00.796Z] Headers:
[2022-05-05T19:30:00.797Z] Server: Microsoft-HTTPAPI/2.0
[2022-05-05T19:30:00.801Z] x-ms-request-id: asdf-asdf-asdf-asdf-60671b000000
[2022-05-05T19:30:00.802Z] x-ms-error-code: AuthenticationFailed
[2022-05-05T19:30:00.809Z] Date: Thu, 05 May 2022 19:29:59 GMT
[2022-05-05T19:30:00.810Z] Content-Length: 422
[2022-05-05T19:30:00.811Z] Content-Type: application/xml
[2022-05-05T19:30:00.812Z] .

Here's my Azure Resource Group:

enter image description here

Here's the function app - you can see I have assigned a user-assigned managed identity to it:

enter image description here

And here are the RBAC roles assigned my managed identity: enter image description here

Questions

Based on my limited knowledge / reading, it feels like I should install Azure.Identity and create some sort of DefaultAzureCredential? https://docs.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme#specifying-a-user-assigned-managed-identity-with-the-defaultazurecredential

EDIT 2

The changes suggested in the answer basically work. To clarify, the setting in local.setting.json that actually works is this:

"[nameofConnectioninC#Method]__serviceUri":"https://[nameOfStorageAccount].queue.core.windows.net/"

This fails when you local debug, but when you publish everything, upstream tests work.



Solution 1:[1]

To use Identity-based connections with Queue Storage, you will need to:

  1. Update you application to use these Nuget Packages: Azure.Storage.Queues and Microsoft.Azure.WebJobs.Extensions.Storage.Queues.

  2. Create an app settings called <CONNECTION_NAME_PREFIX>__serviceUri:

    The data plane URI of the queue service to which you are connecting, using the HTTPS scheme.
    https://<storage_account_name>.queue.core.windows.net

    So in your case you need to create a setting called WidgetStorageQueue__serviceUri

  3. Grant permission to your function app identity to access queue storage. If you just need to send message to a queue, you could use the Storage Queue Data Message Sender role.

I've created a small function app that reproduces this use case.

csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Azure.Storage.Queues" Version="12.9.0" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage.Queues" Version="5.0.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

function file

using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            [Queue("widgetworkspaces"), StorageAccount("WidgetStorageQueue")] ICollector<string> queueCollector,
            ILogger log)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            queueCollector.Add(requestBody);
            return new OkObjectResult(requestBody);
        }
    }
}

settings file

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "WidgetStorageQueue__queueServiceUri": "https://<storage_account_name>.queue.core.windows.net"
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1