Azure Functions の Easy Auth で Azure REST API をユーザー権限で実行する環境を .NET で作ってみた

Azure Functions の認証機能を使ってユーザー権限で何かしらの自動化処理を行う場合、Microsoft Graph API なら簡単に利用できます。しかしながら、Azure REST API を使おうとすると `scope` に設定したい `https://management.azure.com/user_impersonation` はどこに設定するのかわからなかったので、いろいろ試しながら .NET で検証環境を作ってみました。

## ローカルの環境情報

```bash:bash
$ func --version
4.0.4629

$ dotnet --version
6.0.300

$ az version
{
  "azure-cli": "2.43.0",
  "azure-cli-core": "2.43.0",
  "azure-cli-telemetry": "1.0.8",
  "extensions": {}
}
```

## Azure Functions を作成

```bash:bash
prefix=mnrfuncaad
region=japaneast

az group create \
  --name ${prefix}-rg \
  --location $region

az storage account create \
  --name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --sku Standard_LRS

az functionapp create \
  --name ${prefix} \
  --resource-group ${prefix}-rg \
  --consumption-plan-location $region \
  --runtime dotnet \
  --functions-version 4 \
  --storage-account ${prefix}stor \
  --disable-app-insights \
  --https-only \
  --os-type Linux \
  --assign-identity
```

## サービスプリンシパルを作成

```bash:bash
appid=$(az ad app create \
  --display-name ${prefix} \
  --web-redirect-uris https://${prefix}.azurewebsites.net/.auth/login/aad/callback \
  --enable-id-token-issuance true \
  --sign-in-audience AzureADMyOrg \
  --required-resource-accesse '[
    {
      "resourceAccess": [
        {
          "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
          "type": "Scope"
        },
        {
          "id": "37f7f235-527c-4136-accd-4a02d197296e",
          "type": "Scope"
        },
        {
          "id": "14dad69e-099b-42c9-810b-d002981feec1",
          "type": "Scope"
        },
        {
          "id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182",
          "type": "Scope"
        }
      ],
      "resourceAppId": "00000003-0000-0000-c000-000000000000"
    },
    {
      "resourceAccess": [
        {
          "id": "41094075-9dad-400e-a0bd-54e686782033",
          "type": "Scope"
        }
      ],
      "resourceAppId": "797f4846-ba00-4fd7-ba43-dac1f8f63013"
    }
  ]' \
  --query appId \
  --output tsv)

apppw=$(az ad app credential reset \
  --id $appid \
  --append \
  --display-name ${prefix} \
  --years 100 \
  --query password \
  --output tsv)
```

## Azure Fnctions の認証設定

```bash:bash
az webapp auth config-version upgrade \
  --name ${prefix} \
  --resource-group ${prefix}-rg

az webapp auth microsoft update \
  --name ${prefix} \
  --resource-group ${prefix}-rg \
  --client-id $appid \
  --client-secret $apppw \
  --issuer https://sts.windows.net/$(az account show --query tenantId --output tsv)/v2.0 \
  --yes

az resource update \
  --set properties.globalValidation='{
    "redirectToProvider": "azureactivedirectory",
    "requireAuthentication": true,
    "unauthenticatedClientAction": "RedirectToLoginPage"
  }' \
  --ids $(az webapp auth show \
  --resource-group ${prefix}-rg \
  --name ${prefix} \
  --query id \
  --output tsv)

az resource update \
  --set properties.identityProviders.azureActiveDirectory.login='{
    "disableWWWAuthenticate": false,
    "loginParameters": [
      "response_type=code id_token",
      "scope=email openid profile https://management.azure.com/user_impersonation"
    ]
  }' \
  --ids $(az webapp auth show \
  --resource-group ${prefix}-rg \
  --name ${prefix} \
  --query id \
  --output tsv)
```

## ローカルで Functions を作成

```bash:bash
func init ${prefix} --dotnet

cd ${prefix}

func new --name test --template "HTTP trigger" --authlevel "anonymous"

func start
```

## test.cs を編集

```cs:test.cs
using System;
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;
using Newtonsoft.Json;
using System.Security.Claims;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using Microsoft.Identity.Web;

namespace mnrfuncaad
{
  public static class test
  {
    [FunctionName("test")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
        ILogger log)
    {
      HttpClient httpClient = new HttpClient();
      httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + req.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"]);
      var response = await httpClient.GetAsync("https://management.azure.com/tenants?api-version=2020-01-01");
      var responseMessage = response + "\n";
      if (response.StatusCode == HttpStatusCode.OK)
      {
        JObject tenants = JObject.Parse(await response.Content.ReadAsStringAsync());
        foreach (var value in tenants.GetValue("value"))
        {
          responseMessage = responseMessage + "tenant: " + value["tenantId"] + "\n";
        }
      }

      return new OkObjectResult(responseMessage);
    }
  }
}
```

## 必要なパッケージを追加

```bash:bash
dotnet add package Microsoft.Identity.Web

func start
```

## Azure Functions にデプロイして動作確認

```bash:bash
func azure functionapp publish ${prefix}

open https://${prefix}.azurewebsites.net/api/test
```