🗂

Azure Functions v4 + API Management で Azure AD 認証をかけてみる

2021/11/17に公開

.NET 6 も出て Azure Functions v4 も出て色々捗る今日この頃ですが、タイトル通りの構成をぽちぽち作ってみようと思います。
少し前にもやってるんですが、Azure App Service の認証の設定画面が新しくなったりしてるので、変更後でもちゃんと出来るかという自分の確認のためのメモです。

https://zenn.dev/okazuki/articles/0bcab947a5a90cb65c0a

まず、最初にやることは Azure AD で保護された Azure Functions の HttpTrigger の関数を呼び出すという部分をやってみます。
これは画面ぽちぽちでサクッといけるので割と簡単です。やってみましょう。

ドキュメント的にはここの部分になります。

https://docs.microsoft.com/ja-jp/azure/app-service/configure-authentication-provider-aad

適当な Azure Functions のリソースを作ります。Azure Functions を作るときの選択肢に 6 があるので、それを選んでおきましょう。

その部分以外は、基本的にはデフォルトで作りました。アプリも一応デプロイしておきましょう。以下のような簡単な関数を作ってデプロイしておきます。

Function1.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;

namespace FunctionApp1;

public static class Function1
{
    [FunctionName("Function1")]
    public static IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
        ClaimsPrincipal claimsPrincipal,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");
        var name = claimsPrincipal.Identity?.IsAuthenticated ?? false ?
            claimsPrincipal.Identity?.Name :
            "名無し";
        return new OkObjectResult($"Hello, {name}. This HTTP triggered function executed successfully.");
    }
}

認証されていれば、名前入りメッセージが返ってきて認証されていなければ名無しという感じにしました。デプロイ直後にエンドポイントをたたくと以下のようなメッセージが返ってきます。

ばっちりですね。

では、ドキュメントにしたがって認証の構成を行います。作成した Azure Functions をポータルで開いて認証の部分で ID プロバイダーを選択することが出来るので Microsoft を選択して以下のように設定しました。といっても認証されていない要求を 401 に変更したくらいで後はデフォルトです。

認証の構成が終わると、こんな感じの画面になります。

作成されたアプリ名を選択すると Azure AD のアプリの画面に行くのでアプリケーション (クライアント) ID、ディレクトリ (テナント) ID を控えておきましょう。その他に、API の公開に移動してスコープにある api://アプリケーション ID/user_impersonation もコピーして控えておきます。

これで無事認証で保護されるようになってるので Functions を叩くと以下のようにエラーになります。

呼び出し元のアプリの作成

では、ログインして API を叩くコンソールアプリでも作って呼び出しを確認してみます。
Azure AD で新しいアプリを 1 つ登録します。
名前はなんでもいいです。サポートされているアカウントの種類は「 この組織ディレクトリのみに含まれるアカウント (AD のみ - シングル テナント)」にしておきます。(関数側もそうしたので)

クライアント アプリケーションのアプリケーション (クライアント) ID、ディレクトリ (テナント) ID も控えておきます。

クライアント アプリケーションの認証画面でモバイル アプリケーションとデスクトップ アプリケーションをプラットフォームとして追加してリダイレクト URI に http://localhost も設定しておきましょう。

そして API のアクセス許可で、アクセス許可の追加から Azure Functions のアプリケーションの user_impersonation を選択してアクセス許可の追加をします。ついでに AD に管理者の同意を与えますを押すか押さないかはお好みで。同意を与えておくとユーザーがサインインする際に同意画面がスキップされるようになります。

Visual Studio でコンソール アプリのプロジェクトを作って Microsoft.Identity.Client を NuGet から追加して以下のコードを書いてアクセストークンをとってから Azure Functions を叩いてみましょう。

Program.cs
using Microsoft.Identity.Client;
using System.Net.Http.Headers;

// ポータルで作ったクライアント側のアプリの ID とテナント ID
var appId = "クライアントのアプリID";
var tenantId = "Azure AD のテナントID";

// 認証
var app = PublicClientApplicationBuilder.Create(appId)
    .WithRedirectUri("http://localhost")
    .WithTenantId(tenantId)
    .Build();

// ログイン
var authenticationResult = await authenticationAsync(
    app, 
    // Azure Functions のアプリの API の公開で作成した user_impersonation のフルネーム
    new[] { "api://Azure FunctionsのアプリID/user_impersonation" });

// アクセストークンつけて Azure Functions を呼び出し
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
var response = await client.GetAsync("https://関数アプリ名.azurewebsites.net/api/Function1");
Console.WriteLine(response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());

// 認証
static async Task<AuthenticationResult> authenticationAsync(IPublicClientApplication app, IEnumerable<string> scopes)
{
    var account = (await app.GetAccountsAsync()).FirstOrDefault();
    try
    {
        return await app.AcquireTokenSilent(scopes, account)
            .ExecuteAsync();
    }
    catch (MsalUiRequiredException)
    {
        return await app.AcquireTokenInteractive(scopes)
            .ExecuteAsync();
    }
}

これを実行して、私の Microsoft アカウントでサインインすると以下のような結果が返ってきました。

OK
Hello, k_ota28@hotmail.com. This HTTP triggered function executed successfully.

ばっちりですね。

API Management を Azure Functions の手前に置こう

では、間に API Management を置きます。
適当な API Management を作って、先ほど作った Azure Functions を API として取り込みます。取り込み方はいくつかありますので、以下のドキュメントあたりを参考にやるのがいいと思います。

https://docs.microsoft.com/ja-jp/azure/api-management/import-function-app-as-api

とりあえず API Management でインポートするだけなので、その部分のスクショは省きます。
とりこんだら、API Management の Settings 画面で Subscription required の項目のチェックを外します。今回は Azure AD のトークンの妥当性検証が通れば通したいので API Management のサブスクリプション キーがなくても動くようにするためです。チェックが入っているとサブスクリプション キーも含めてリクエストを送らないと認証が通りません。

API Management でインポートした Azure Functions の All operations のポリシーを以下のようにします。

<policies>
    <inbound>
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized on APIM">
            <openid-config url="https://login.microsoftonline.com/Azure ADのテナントID/.well-known/openid-configuration" />
            <required-claims>
                <claim name="aud" match="all">
                    <value>api://Azure FunctionsのAppId</value>
                </claim>
            </required-claims>
        </validate-jwt>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

これで、トークンの検証をしてくれるはずです。試してみましょう。API を呼ぶコンソールアプリケーションに書いていた URL を API Management 越しで Azure Functions を呼ぶための URL にします。具体的には以下のような感じになります。

var response = await client.GetAsync("https://API Management名.azure-api.net/関数アプリ名/Function1");

この状態で実行して以下のような結果が返ってきたら成功です。

OK
Hello, k_ota28@hotmail.com. This HTTP triggered function executed successfully.

試しに Bearer トークンを設定している行をコメントアウトすると以下のような結果になったので、ちゃんと API Management が素通りではないことが確認できます。

Unauthorized
{ "statusCode": 401, "message": "Unauthorized on APIM" }

Azure Functions には API Management からしかの通信しか許さない!

ここまでやりたい場合は API Management のコンサンプションプラン以外を選んで API Management に紐づく IP アドレスからしか通信を受け入れないように構成を行います。

下のドキュメントを参考にすると出来ると思います。パット見 API Management のサービスタグはなさそう?あったらいいなぁ。

https://docs.microsoft.com/ja-jp/azure/app-service/app-service-ip-restrictions

まとめ

とりあえず動いてよかった。

Microsoft (有志)

Discussion