このエントリは2021/11/02現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。
Azureには、ユーザー・パスワードを使わずにリソースを認証・認可できるManaged Identity(マネージドID)という仕組みがある。これを使えば、特定のリソースからのみアクセスできるように構成できる。ドキュメントでは、Storageへアクセスしたり、(例えばSQL Databaseなどが持つ)リソースのFirewallを潜ってリソースにアクセスしたりするために利用する、といったユースケースの説明がある。
Azure リソースのマネージド ID とは / What are managed identities for Azure resources?
https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
App ServiceやAzure Functionsも同じくAzureのリソースなので、両者間のHTTPベースのアクセスにおける認証においても、Managed Identityを利用できる。
やり方は、Authorizationヘッダーにトークンを付けるという、通常のREST APIの認証と何も変わりはないが、Managed IdentityからBearerトークンを取得する箇所について備忘録として記載しておく。
作成しようとしているFunction app
以下が今回実現しようとしている構成。

今回は、呼び出し元、呼び出し先をそれぞれJava、C#で実装することにする。Java > Java、C# > C#が呼び出せるのは当然として、C# > Java、Java > C#が可能かも確認しておく。
アプリケーションの構成
今回利用するのはAzure Functions。もちろんApp Serviceでも問題ない。サクッと作れること、認証の仕組みはApp Serviceと同じことから、今回はFunctionsを使う。
1. Function appの作成
Portalからでも、Azure CLIからでも可。今回はfunc-cs*はC#(.NET 6) on Windows、func-j*はJava (Java 11)on Linuxを選択しているが、OS、ランタイムのバージョンとリージョンは何でもよい。
呼び出し側のFunction app(func-cs01、func-j01)は、Query parameterで呼び出し先(func-cs02、func-j02)を切り替えることができるようにしている。c02、j02でそれぞれfunc-cs02、func-j02のFunction appを呼び出すようにしている。
呼び出し側のFunction appからのレスポンスは共通で、func-*02を呼び出したときのHTTPステータスコードと、func-*02からのレスポンスメッセージを含む。以下はfunc-cs01の例。この例では、func-cs02を呼び出した際のレスポンスとHTTPステータスコードを返している。
{
"response": "This is responded by function [auth-cs] in func-cs02. It means HTTP triggered function executed successfully.",
"status": 200
}
2. Managed Identityの有効化
Managed Identityは、func-cs01、func-j01のみ有効にする。ユーザー割り当て済み(User assigned)とシステム割り当て済み(System assigned)のいずれでもかまわないが、今回はリソースライフサイクルとIdentityのライフサイクルが一致することからシステム割り当てを使っている。
3. 認証の構成
認証はfunc-cs02、func-j02でのみ有効にする。Azure Portalで【設定】>【認証】> 【IDプロバイダーを追加】ではMicrosoftを選択(つまりAzure Active Directoryを使う、ということ)。

以後の設定は以下の通り。
- 【アプリの登録の種類】は「アプリの登録を新規作成する」を選択
- 【サポートされているアカウントの種類】は「シングルテナントアプリケーション」
- 【認証されていない要求】は「HTTP 401 認可されていない: API に推奨」を選択。
- 【アクセス許可】もデフォルトのまま、つまりスコープは一旦デフォルトの
user.read
を使う。
アプリケーションIDはManaged IdentityでBearerトークンを取得する際に使うのでメモしておく(あとから参照することもできる)。
この段階でfunc-c02やfunc-j02を単純に呼び出す(つまりAuthorizationヘッダーを付けずに呼び出す)と、以下のように401が返ってくる(C#だとメッセージがついてくる)。


4. コード
a) func-cs01 と func-j01
呼び出し元はManaged Identityからトークンを取得する必要があるため、そのコードを追加しなくてはいけない。C#の場合は
string audienceId; // アクセス対象(func-cs02もしくはfunc-j02)APIのApplication ID
string accessToken = await new AzureServiceTokenProvider().GetAccessTokenAsync(audienceId);
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
Javaの場合は以下をそれぞれ追加する。今回はSystem assigned identityを使っているので、User assigned identityのようにclientId()
メソッドを使ってClient IDを指定しなくてもよい。
// Scopeは、 api://{Application ID} でURIを記述し、その後ろにスコープを指定。今回は .defaultを利用
// このApplication IDは、アクセス対象(func-cs02もしくはfunc-j02)APIのApplication ID
TokenRequestContext tokenRequestContext = new TokenRequestContext().addScopes("api://<Application ID>/.default");
// ローカルでテストするなら、DefaultAzureCredentialを使う
DefaultAzureCredential defaultAzureCredential = new DefaultAzureCredentialBuilder().build();
String accessToken = defaultAzureCredential.getToken(tokenRequestContext).map(AccessToken::getToken).block();
// ManagedIdentityCredentialを使うこともできる。
ManagedIdentityCredential managedIdentityCredential = new ManagedIdentityCredentialBuilder().build();
String accessToken = managedIdentityCredential.getToken(tokenRequestContext).map(AccessToken::getToken).block();
これでBearerトークンを取得し、Authorizationヘッダーに設定する。
b) func-cs02とfunc-j02
特にJWTをチェックする必要がないのであれば、プラットフォーム側がよろしくやってくれるので、テンプレートやarchetypeで生成したもののままでかまわないが、JWTのチェックをするのであれば、AuthorizationヘッダーのBearerトークンを調べる。なおHTTPヘッダーのキーはすべて小文字なので注意。
なお、func-*02に渡ってくるJWTはaud(Audience ID)とappid (Application ID)はそれぞれfunc-*02、func-*01を表すApplication IDが入っている。
テスト
func-cs01からfunc-cs02を呼び出す場合。

func-cs01からfunc-j02を呼び出す場合。

func-j01からfunc-cs02を呼び出す場合。

func-j01からfunc-j02を呼び出す場合。

今後
これだけだといまいちなので、Application Roleを使ってより細かく制御した場合についても今後調べる予定(いつになるかは知らんけど)。
おまけ
このサンプルのコードは以下にホストしている。