シークレットを使わずにテナント間でアクセスする

昨年末ぐらいに Microsoft EntraのFederated Identity Credentials(以下FIC)としてのManaged Identities(以下MID)がPreviewで使えるようになりました。

シークレットや証明書などを使わずにMIDを使ってEntraテナントで保護されたAzureやMicrosoft Graph APIなどのリソースにアクセスすることができます。

今回はこの仕組みを使ってEntraテナント間でMIDを使ってアクセスしたいと思います。

概要

これまではMIDを使ってアクセスできるのはMIDをRBACで追加できるAzureリソースぐらいでしたが、今回追加されたFICを使うことでMIDの利点を生かしたまま柔軟にMicrosoft Entraで保護されたリソースにアクセスできます。しかも応用すればテナントを跨いだアクセスも行えるようになります。今まではMicrosoft Entraで保護されたリソースなどにアクセスする場合はサービスプリンシパルを作って証明書やシークレットを用いて認証を行い、対象リソースへアクセスしたと思いますが、その管理から解放される仕組みです。

具体的な仕組みはワークロードIDフェデレーションフローと呼ばれるものを使ってアクセストークンの交換を行うことで実現します。

ざっくりフローの流れを説明するとMIDを使うアプリがFIC用のMIDのトークンを取得、そのトークンを対象リソースへのアクセストークンに交換することで実現します。(トークンの交換そのものはOAuth 2.0のToken Exchangeと思います)
MIDを得るときはMIDなのでシークレットなどは不要、FICで交換する際はあらかじめフェデレーションの資格情報を構成しておくことでここもシークレット不要、という感じでシークレット不要で最終的なアクセストークンを得ることができるようになっています。

サンプルの構成

では実際に試す前に前提となる構成を確認しておきます。ざっくり以下のような環境想定です。

MIDを割り当てたアプリ(Functions)が内部で別テナントのAI FoundryのAgentを呼ぶ、みたいなシナリオです。まぁ相手側は何でもいいのですが。ようはMIDを使ってテナントを超えるテストができればOKです。

事前準備

さて前提条件がわかったところで進めるわけですが、何も構成せずにできれば楽ですがさすがにそれはガバガバなので無理です。リソース側が正しいアクセスか判定する根拠が何もないので。
ということで上記構成を基に必要な設定を進めていきましょう。

アプリ側テナント

最初はアプリ側テナントでMIDを使うように構成します。この時使うのは「ユーザー割り当てマネージドID」です。システム割り当ては実体がない等特殊なのでこのシナリオでは利用できません。(分離スコープは無しで作っておきます。リージョンで試してないけどテナントとか考えると微妙)

次にリソース(例だとAI Foundry)にアクセスするためのアプリをMicrosoft Entra登録します。特に悩むことはないですが、今回のシナリオだとテナント間アクセスをするのでアカウントタイプを「マルチテナント」で作成します。

作ったアプリの「証明書とシークレット」で「フェデレーション資格情報」を選択して「資格情報の追加」を行います。

追加画面で「フェデレーション資格情報のシナリオ」は「その他の発行者」、「発行者」はアプリテナントのIssuer( https://login.microsoftonline.com/{アプリ側テナントID}/v2.0 の形式)、種類は「明示的なサブジェクト識別子」にして値にユーザー割り当てマネージドIDのObject(Principal)IDを指定します。

※ 書いてて思ったのですがシナリオにManaged IDが使えるようになってるのでユーザー割り当てマネージドID使う必要もないし、選択するだけでOKな気がしてきました。(Previewでてから半年で増えたポイ)まぁ他の発行者のやり方知っていればどうとでも応用が利くので今回はこのまま進めます。(例えばEntra以外のIdPと連携するときとか)

ここで何をしているかというと、このサービスプリンシパルの認証を行う際に提示されるトークンについて設定しているわけです。なのでアクセスしようとしてるユーザー割り当てマネージドIDにて発行されるトークンの情報を入力しています。(発行者はトークンのIssuer(iss)、値はsubです)
なので接続するアカウントの値のところはひな形のIDであるAppIDではなく、実体のObjectIDを指定する必要があります。
対象ユーザーはFICで交換する際指定することになるscopeになります。編集せず既定のままでいいと思います。(既定のスコープにする場合、 api://AzureADTokenExchange/.default とかになります)

※余談ですがスコープが最後スラッシュで終わる場合に既定(.default)を使うときは api:/hogehoge//.default みたいにスラッシュ重なることがあるようですね(知らなかった)

さてアプリ側はこれで準備が整いました。作成したサービスプリンシパルのAppID(Client ID)は控えておきましょう。

リソース側テナント

リソース側テナントではアプリ側で登録されたサービスプリンシパルを信頼できるように、エンタープライズアプリケーションとして登録します。既に存在しているアプリケーションをエンタープライズアプリに登録する方法はポータルでは提供されてないようなので、CLIを使用する必要があります。

New-AzADServicePrincipal -ApplicationId "52b...cd5"

PowerShellであれば New-AzADServicePrincipal を使ってアプリテナント側で登録したアプリのAppID(Client ID)を使って登録します。AppIDはひな形のIDなので、アプリテナント側でひな形を使って実体のリソーステナントにも作るという感じです。
あとは登録されたこのエンタープライズアプリ(つまりサービスプリンシパル)に対してRBACなどで権限を付与すれば準備は完了です。準備完了後は以下のような構成になってると思います。

アクセスしてみる

準備も整ったことだし、実際にトークンを取得してみましょう。今回はC#のAzure.Identityを使いますが仕組み自体は一般的なOIDCとOAuth2.0 Token Exchangeなのでどうとでもなると思います。

ざっくりすることは、ユーザー割り当てマネージドIDからFICに対してアクセストークンを得る、得たトークンを交換する、交換したトークンを使ってリソースにアクセスする、の3点です。

最初にユーザー割り当てマネージドIDは以下のような感じで取得できます。ManagedIdentityCredentialを作る際にユーザー割り当てマネージドIDを渡すだけですね。

var mid = new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId("a83...635"));
var tokenRequestContext = new Azure.Core.TokenRequestContext(["api://AzureADTokenExchange/.default"]);
var accessToken = await mid.GetTokenAsync(tokenRequestContext);
// accessToken.Token にアクセストークンが入ってる

上記コードで得られるトークンの中身は以下のような感じ。発行者がアプリ側テナントでsubなどがMIDというこでMIDに対するトークンというのがわかります。ちなみにAudienceのアプリIDは fb60f99c-7a34-4190-8149-302f77469936 で、これは AAD Token Exchange Endpoint です。
サービスプリンシパルのフェデレーション資格情報で設定した内容の通りですね。(トークン取得する際のコンテキストのScopeに api://AzureADTokenExchange/.default を渡すのを忘れずに)

次はこのアクセストークンを対象リソースにアクセスするためのトークンに交換する必要があります。SDKであれば ClientAssertionCredential を使うことで簡単に行えます。

ClientAssertionCredential cac = new ClientAssertionCredential(
    "5e3...ed1", //リソース側テナントID
    "52b...cd5", //サービスプリンシパルのクライアントID
    async (token) =>
    {
           // 前述の通りユーザー割り当てマネージドIDからトークンを得る
            var tokenRequestContext = new Azure.Core.TokenRequestContext(["api://AzureADTokenExchange/.default"]);
            var accessToken = await mid.GetTokenAsync(tokenRequestContext).ConfigureAwait(false);
            return accessToken.Token;
    });

var token = cac.GetToken(new Azure.Core.TokenRequestContext(["https://ai.azure.com/.default"]));

最終的に得られた交換後のトークンを見てみます。今回交換先のテナントはリソーステナントのほうなので、トークンのIssuerやsubはリソーステナント側のサービスプリンシパルのものになっていて、Audienceはアクセス先のリソース(今回ならAI Foundry)になってます。これでリソーステナント側の対象リソースは正しいトークンとして処理できるものになりました。

※同じ理屈で同一テナントでもAzure以外のリソースに対するトークンを得てアクセスできるようになるというわけです。

REST API叩いたりしたら問題ないことがわかります。今回みたいにAI FoundryのアクセスをC#でする場合はTokenCredentialを渡しているところにClientAssertionCredentialを渡してあげれば内部で自動的にいろいろ処理されます。(MSALなどでも大差ないかと)

AgentsClient = new AIProjectClient(
  new Uri("AI Foundry projectのURL"),
  cac // TokenCredential
).GetPersistentAgentsClient();

まとめ

サービスプリンシパルなどややこしい部分が多いですが、実際にやってみれば意外と簡単だと思います。テナント間でシークレットのやり取りもしなくていいですし、メリットしかないかと。FICの設定もBicepでできるようになってるので、構成がかなりしやすいと思います。パターンの1つとして覚えておいて損はないですね。

コメントを残す