ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure App Service の Easy Auth を有効にした Web Apps から Functions にアクセスする

Azure App Service では、組み込みの認証機能が提供されています。この機能は Easy Auth と呼ばれていて、アプリケーション側に最低限のコードを実装するだけで、Azure AD などのさまざまなプロバイダーを使って保護できます。
docs.microsoft.com
今回は、Web Apps と Functions で Easy Auth を有効にして、Azure AD で認証されたフロントエンドの Web アプリケーションからバックエンドの API を呼び出します。

  • Web Apps で ClaimsPrincipal を取得する
  • Functions のアクセストークンを取得するように構成する

この2つの構成がポイントになるので、まとめておきます。

Azure App Service の Easy Auth を有効にする

Azure ポータルを使って、Web Apps と Functions の Easy Auth を有効にします。
認証プロバイダーに Azure AD を選択して設定する際に Express モードを選択すると、Azure AD のアプリケーションも同時に登録できます。


バックエンドの API をデプロイする

Functions には、Visual Studio 2019 のプロジェクトテンプレートで作成した Http Trigger の Functions をデプロイします。
Azure AD 認証するので、Functions の AuthorizationLevel は Anonymous に変更しておきます。

[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    string name = req.Query["name"];
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;

    // ClaimsPrincipal のユーザー名を取得
    var identityName = req.HttpContext.User.Identity.Name;

    var responseMessage = string.IsNullOrEmpty(name)
        ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
        : $"Hello, {name}. IdentityName is {identityName}.";
    return new OkObjectResult(responseMessage);
}

Functions の場合、ClaimsPrincipal を自動的に HttpContext.User に格納してくれますので、ユーザー名などを取得できます。

フロントエンドの Web アプリケーションをデプロイする

Web Apps には、Visual Studio 2019 のプロジェクトテンプレートで作成した ASP.NET Core 3.1 の MVC をデプロイします。
Easy Auth が設定してくれる HTTP ヘッダーの X-MS-TOKEN-AAD-ACCESS-TOKEN にアクセストークンが格納されています。
docs.microsoft.com
このアクセストークンを使って、バックエンドの API を呼び出すコードを実装しておきます。

public async Task<IActionResult> Privacy()
{
    var client = _clientFactory.CreateClient();
    if (HttpContext.Request.Headers.ContainsKey("X-MS-TOKEN-AAD-ACCESS-TOKEN"))
    {
        var accessToken = HttpContext.Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"];
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    }
    var response = await client.GetAsync($"https://gooner-api.azurewebsites.net/api/Function1?name=gooner");
    if (response.StatusCode == HttpStatusCode.OK)
    {
        ViewData["ApiResult"] = await response.Content.ReadAsStringAsync();
    }
    else
    {
        ViewData["ApiResult"] = $"Invalid status code in the HttpResponseMessage: {response.StatusCode}.";
    }
    return View();
}

ログインしたユーザー名とサインアウトをヘッダーに追加しておきます。Views/Sharedディレクトリに _LoginPartial.cshtml を追加し、_Layout.cshtml に組み込みます。
Easy Auth では、サインアウトのエンドポイントに /.auth/logout を使います。

@using System.Security.Principal

<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
    <li class="nav-item">
        <span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" href="/.auth/logout">Sign out</a>
    </li>
}
</ul>

Web Apps の場合、ClaimsPrincipal が自動的に HttpContext.User に格納されません。従来の ASP.NET の場合は、ClaimsPrincipal が自動的にバインドされていました。
ASP.NET Core の場合、アプリケーション側での対応が必要となります。

Web Apps で ClaimsPrincipal を取得する

前述の通り、ASP.NET Core では ClaimsPrincipal を取得するために、アプリケーション側での対応が必要となります。
docs.microsoft.com
HTTP ヘッダー(X-MS-CLIENT-PRINCIPAL-NAMEなど)を解析するか、/.auth/me エンドポイントから取得するか、どちらかの方法になります。
これらの機能を実装する ASP.NET Core 向けの認証ミドルウェアは公式には提供されていませんが、一時的な回避方法として MaximeRouiller.Azure.AppService.EasyAuth が提供されているので、今回はこちらを使います。
github.com

認証ミドルウェアの NuGet パッケージをインストールします。

PM>Install-Package MaximeRouiller.Azure.AppService.EasyAuth -Version 1.0.0-ci141

Startup クラスで、ミドルウェアパイプラインに Easy Auth の ClaimsPrincipal を取得する認証ミドルウェアを追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient();

    // Easy Auth の ClaimsPrincipal を取得する認証ミドルウェア
    services.AddAuthentication().AddEasyAuthAuthentication((configure) => {});

    services.AddControllersWithViews();
}

Functions のアクセストークンを取得するように構成する

HTTP ヘッダーの X-MS-TOKEN-AAD-ACCESS-TOKEN にアクセストークンが格納されていますが、このままでは Functions の認証が通りません。
docs.microsoft.com
フロントエンドの Web アプリケーションを認証する際に、バックエンドの API を呼び出すトークンも含めるように構成する必要があります。
Azure ポータルから構成できないので、Azure Resource Explorer を使います。
Azure Resource Explorer から Web Apps の authsettings で、additionalLoginParams を構成します。

"additionalLoginParams": [
  "response_type=code id_token",
  "resource=<Azure AD に登録された Functions の Application (client) ID>"
],

実行結果

Azure AD で認証されたフロントエンドの Web アプリケーションから、バックエンドの API を呼び出せることを確認します。

Web Apps と Functions で ClaimsPrincipal を取得できました。

まとめ

Web Apps と Functions で Easy Auth を有効にして、Azure AD で認証されたフロントエンドの Web アプリケーションからバックエンドの API を呼び出しました。
アプリケーション側で認証機能を実装せずとも、要件を満たすのであれば、Easy Auth をうまく活用する選択肢もありだと思います。