🔒

Azure Functions で Azure App Service 認証を使わずに Web API を保護する

に公開

はじめに

Azure Functions で Web API を保護するときには Azure App Service 認証や API Management を使用するのが一般的です。しかしこの方法ではローカルでのデバッグがしにくいという問題点があります。ほかの Web API を呼び出すシナリオでは、On-Behalf-Of フローを使ってアクセス トークンを変換することもあります。このとき、Azure のリモートでしか動作を確認できないのは大変なので、できればローカルで完結させたいところです。

Azure Web Apps の場合は ASP.NET Core の認証を使ってほかの Web API を呼び出す Web API を簡単に実装できます。

https://learn.microsoft.com/ja-jp/entra/identity-platform/scenario-web-api-call-api-app-configuration

Azure Functions では ASP.NET Core の認証を使用できないためこの方法は使用できません。代わりに Microsoft.Identity.Web で提供されている AuthenticateAzureFunctionAsync 拡張メソッドを使うことで認証を実装できます。Microsoft.Identity.Web における Azure Functions のサポートについては以下に記載があります。インプロセスの記述になっていますが分離プロセスでも動作します。

https://github.com/AzureAD/microsoft-identity-web/wiki/Azure-Functions

サンプル コード

https://github.com/karamem0/samples/blob/main/azure-functions-on-befalf-of-flow

実行手順

appsettings.json

Microsoft Entra アプリケーションの設定情報を記載します。

{
  "MicrosoftEntra": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "{{client-id}}",
    "ClientSecret": "{{client-secret}}",
    "TenantId": "{{tenant-id}}"
  }
}

Program.cs

既定では Azure Functions は appsettings.json を読み込まないので AddJsonFile メソッドを追加します。AddMicrosoftIdentityWebApiAuthentication メソッドを呼び出して認証を構成します。後述する Microsoft Graph API の呼び出しで使うため MicrosoftIdentityOptions を取得できるようにしておきます。

var builder = FunctionsApplication.CreateBuilder(args);

_ = builder.ConfigureFunctionsWebApplication();

var configuration = builder.Configuration;
_ = configuration.AddJsonFile("appsettings.json", true, true);

var services = builder.Services;
_ = services.AddApplicationInsightsTelemetryWorkerService();
_ = services.ConfigureFunctionsApplicationInsights();
_ = services.Configure<MicrosoftIdentityOptions>(configuration.GetSection("MicrosoftEntra"));
_ = services.AddMicrosoftIdentityWebApiAuthentication(configuration, "MicrosoftEntra");

var app = builder.Build();
await app.RunAsync();

SampleFunction.cs

AuthenticateAzureFunctionAsync メソッドは認証の結果 (真偽値) および認証に失敗したときのレスポンスを返します。認証に成功したときは Authorization ヘッダーから渡されるアクセス トークンを取得し GraphServiceClient を初期化します。GraphServiceClient が Azure Identity を受け入れるようになったので初期化が簡単になりました。ASP.NET Core の場合は Microsoft.Identity.Web の拡張メソッド (EnableTokenAcquisitionToCallDownstreamApi メソッドおよび AddMicrosoftGraph メソッド) が使えますが、Azure Functions ではできないようです。

public class SampleFunction(IOptions<MicrosoftIdentityOptions> options)
{
    private readonly MicrosoftIdentityOptions options = options.Value;

    [Function("SampleFunction")]
    public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "GET", "POST")] HttpRequest request)
    {
        var (authenticationStatus, authenticationResponse) = await request.HttpContext.AuthenticateAzureFunctionAsync();
        if (authenticationStatus)
        {
            var authorizationHeader = request.Headers.Authorization.FirstOrDefault()?.Split(" ");
            if (authorizationHeader?.Length == 2 && authorizationHeader[0] == "Bearer")
            {
                var credential = new OnBehalfOfCredential(
                    options.TenantId,
                    options.ClientId,
                    options.ClientSecret,
                    authorizationHeader[1]
                );
                var graphServiceClient = new GraphServiceClient(credential);
                var currentUser = await graphServiceClient.Me.GetAsync();
                return new OkObjectResult(currentUser);
            }
            else
            {
                return new UnauthorizedResult();
            }
        }
        else
        {
            return authenticationResponse ?? new UnauthorizedResult();
        }
    }
}

おわりに

ASP.NET Core 統合により分離プロセスでも HttpRequest クラスや HttpResponse クラスが使えるようになったのでとても便利になりました。本格的な Web API では Azure Web Apps を使うべきですが、ほかのトリガーと組み合わせる場合に Azure Functions を使う局面はあるので、覚えておきたいです。

Discussion