異なるテナントにあるAzureリソースにアクセスするベストな方法～フェデレーションID資格

## はじめに

Azure上で、あるテナントのコンピューティングサービス（例: Azure Functions）などから、別のテナントにあるリソース（例: Storage Account）にアクセスする際の設定について、簡単な備忘録として記載します。

通常、同一テナント内でAzure FunctionsからStorageにアクセスする場合は、マネージドIDとRBACを使用すれば非常に簡単です。しかし、テナントを横断してアクセスする場合、マネージドIDは使用できません。

この記事では、大まかな流れと、設定の際に必要な項目などを、図を使って解説します。スクリーンショットなどを多用した手順に従って作業をする際に、参考になれば幸いです。

まずは、具体的に何をしようとしているかを図で見ていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/f387f032-578e-742c-dea5-4fac22d696e6.png)

テナントXに（A）Azure function があり、そこから別のテナントYにあるStorage accountリソースにアクセスしたいとします。

やりたいことは単純なのですが、テナントを跨ぐために「マネージドIDを信頼するようにアプリを構成」しなくてはなりません。

## マネージドIDを信頼するようにアプリを構成する

下図は、マネージドIDを信頼するようにアプリを設定するために必要な設定項目の全体像です。それぞれの項目にアルファベットのラベルを付けました。まずは大まかに見ていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/1d23260e-0db0-d25c-3bf3-ccef536f32c8.png)

（X）**アプリケーションを登録したテナント**： テナントを跨いでアクセスする際にはテナントの識別子IDが重要になります。どのように使われるかは後述します。

（C）**「ユーザ割り当て」タイプのマネージドID**： これは同じテナント内にあるリソースへアクセスする場合でも推奨されています。マネージドIDの説明は割愛しますが、これのおかげでパスワードや証明書や期限切れなどの頭痛の種から解放されることになります。詳細設定については```Client ID```と```Object ID```のふたつがあり、混乱の原因になりやすいので、その点を含めて後ほど詳しく解説します。

（D）**Entra ID アプリケーション**： テナントX内で登録するアプリケーションです。マネージドIDはテナント内でしか意味を持たないので、テナント外でマネージドIDの代りになるものが必要です。

（E）**フェデレーションID資格情報**： これを設定することで外部のIDプロバイダーとフェデレーション（信頼関係）を構築します。ここは最も重要な部分で、特に設定について迷う部分でもあります。以下のような詳細設定がありますが、詳細は後述します。

（F）**IDプロバイダー**： ここではMicrosoft Entra のテナントそのものがIDプロバイダーですが、外部IDプロバイダーを設定することもあります。例えばGithubやGoogleをIDプロバイダーとして設定する構成もあります。このプロバイダーがトークンを発行してくれます。詳細は後述します。

（Y）**リソースがあるテナント**： このテナントにあるリソースへアクセスするので、テナントXと同様に識別子が重要になります。

（G）**サービスプリンシパル**： これはテナントX側で登録されたアプリケーションの、テナントY内でのインスタンスです。サービスプリンシパルを作成（プロビジョン）する方法は様々で、ケースバイケースなので、この点は割愛します。

（H）**RBAC Role付与**： テナントY内でサービスプリンシパルにRoleを設定することで、サービスプリンシパルがAzureリソースにアクセスできるようになります。

## フェデレーションID資格を設定する

この設定がキモになります。特に、以下の３つの項目が重要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/e1082d60-9fcf-13d7-bfee-fd7b704a7706.png)

### Issuer:
値はマネージドIDが設定されているテナントXの識別子を含んだ、Entra ID authority のURLです。

```https://login.microsoftonline.com/{テナントXの識別子}/v2.0```


Issuerはフェデレーションを構成する際のIDプロバイダーを指し、この場合はMicrosoftが実装するOAuth2.0 (OpenID Connect)のエンドポイントです。

リソースにアクセスするために必要な「アクセストークン」を取得する際に、IDプロバイダーが確認する項目のひとつです。

かならず```/v2.0```で終わらすことや、スペース文字などを含まないように注意しましょう。

### Subject:
値はマネージドIDの```Object ID```です。```Client ID```は間違いなので気を付けましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/dc36d0c2-013b-b5b8-d0fb-79d44b58ed61.png)

この値は「マネージドIDのトークン」を「アクセストークン」に**交換**するリクエストをIDプロバイダーに送る際、IDプロバイダー側が確認する項目です。

### Audience
値は
```api://AzureADTokenExchange ```
です。（政府クラウドなどは異なる値です）

通常は対象のユーザーやリソースを表しますが、この場合は「トークンを交換する」目的なのでこの値を設定します。トークンを交換する際に送信する**ひとつめのトークン**はこれをaudienceとします。

### フェデレーションID資格情報の効果

テナントX側の設定は以上でが、これによって、テナントX側のAzure Functionが「マネージドIDの資格を表すトークン」を「テナントY側にあるリソースにアクセスする目的のトークン」に交換することが出来るようになりました。

ただ、テナントX側の設定は終わりましたが、リソーステナントであるテナントY側での設定が残っています。

## リソーステナントを準備する

### サービスプリンシパルをプロビジョニングする
フェデレーションID資格を利用した、別のテナントにあるリソースにアクセスする仕組みでは、この「（G）サービスプリンシパル」が得に重要です（どの設定もすべて重要ですが・・・）。

サービスプリンシパルをテナントYで設定する方法は、テナントYの管理ポリシーや権限の設定に依存しているので、一概にどのように設定するかは言えません。私の最近の実装では、私はテナントの管理者権限を持っていませんから、リクエスト提出して承認を受けるプロセスを踏みましたが、やり方は組織によりけりでしょう。

このサービスプリンシパルが、テナントX側の「（D) Entra ID アプリケーション」を代表することになります。

### サービスプリンシパル（アプリ）に権限を付与する

ここでは「（G）サービスプリンシパル」に、「（B）Azureリソース Storage Account」へのアクセスを許可するRBAC設定が必要です。この詳細はサービス・製品の要求によりけりなので、必要に応じて設定します（最低限の権限を渡す、という原則を忘れずに）。

以上でテナントY側の準備が終了しました。
いよいよ「（A）Azure function」から「（B）Azureリソース Storage Account」に、フェデレーションID資格を利用してアクセスします。

## フェデレーションID資格を使ってアクセストークンを取得する

下図の「赤丸」の付いた線は、Azure functionがアクセストークンを取得し、それをそれてリソースにアクセスする流れを示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103139/63493fae-2c9d-f4e3-e6b8-97ea54e01d7b.png)

まずは大まかな流れを見てみましょう。

**a1**: まずはマネージドIDの資格を得るために、テナント内の「Azure インスタンス メタデータ サービス」に対してトークンを要求します。「Azure インスタンス メタデータ サービス」は意識しなくてもライブラリ各種の下に隠れています。

**a2**: 次に「マネージドIDのトークン」を、IDプロバイダーに対して送信して「テナントYにあるリソースへのアクセストークン」を要求します。このステップで「トークンの交換」が行われます。

**a3**: アクセストークンを取得したら、それを```Authorization``` ヘッダーなどにそえてAzureリソースにアクセスします。

このアクセストークンは「（D) Entra ID アプリケーション」の変わり身である「（G)サービスプリンシパル」として、RBAC設定に従ってテナントY内のリソースへのアクセスを許可します。

一方、テナントX側では「（E）フェデレーション資格情報」が「（C）ユーザ割り当てマネージドID」をSubjectとして指定していたので、そのマネージドIDとして実行している「（A）Azure function」のコードが「（B)Azure storage account」にアクセスできるというわけです。

## アクセストークンを取得して、Storage にアクセスしてみよう

具体的なコードは以下のページにあります。特に３つの異なるアプローチで利用できるのが分かります。

https://learn.microsoft.com/ja-jp/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center

以下は、そのコードを、ラベルを使って書き直したものです

```
string storageAccountName = "<(B)Azureリソース storage accountの名前>";
string containerName = "<(B)のコンテナ名>";
        
// ここは混乱しやすいですが、テナントX側のEntra ID アプリのClient IDを指定します（Object IDはそもそもありません)
string appClientId = "<(D)Entra ID アプリケーションのClient ID>";

// リソーステナントの識別子
string resourceTenantId = "<(Y) テナントYの識別子>";

// これも混乱しそうですが、Object ID ではなく Client IDです。
string miClientId = "<(C)ユーザ割り当てマネージドIDのClient ID>"; 

// パブリッククラウドの場合
string audience = "api://AzureADTokenExchange";

// アクセストークンを取得
// コールバックで「マネージドIDトークン」をまず取得していることに留意。
var miCredential = new ManagedIdentityCredential(managedIdentityClientId);
    ClientAssertionCredential assertion = new(
    resourceTenantId,
    appClientId,
    async (token) =>
    {
        // コールバック内でマネージドIDのトークンを取得
        // audience がトークン交換になっていることに留意。
        var tokenRequestContext = new Azure.Core.TokenRequestContext(new[] { $"{audience}/.default" });
        var accessToken = await miCredential.GetTokenAsync(tokenRequestContext).ConfigureAwait(false);
        return accessToken.Token;
    });

// アクセストークンを使ってリソースにアクセスする
var containerClient  = new BlobContainerClient(new Uri($"https://{storageAccountName}.blob.core.windows.net/{containerName}"), assertion);

// 適当にリソースを使うコード
await foreach (BlobItem blob in containerClient.GetBlobsAsync())
{
    BlobClient blobClient = containerClient.GetBlobClient(blob.Name);
    Console.WriteLine($"Blob name: {blobClent.Name}, uri: {blobClient.Uri}");            
}
  
```

## まとめ

設定内容のボリュームは大したことないですが、いくつか間違いやすいところ、混乱しやすい部分があります。

またトークンを交換するまで設定が間違っていることが分からないなど、設定を完了するまでに多少つまづくかもしれませんが、全体の仕組みが理解できればトラブルシューティングも少しは楽になるとおもいます。