API Management と Azure Functions で AAD 認証

7 min read読了の目安(約6300字

EasyAuth で認証した Functions + API Management

  1. Function App 作成 (fun-okazukiauth1 という名前で作成しました)
  2. API Management を作成 (apim-okazukiauth1 という名前で作成しました)
  3. 関数を作成
    1. Anonymous な認証レベルで Function1 という名前の関数を作成
      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;
      using System.Linq;
      
      namespace Auth1
      {
          public static class Function1
          {
              [FunctionName("Function1")]
              public static IActionResult Run(
                  [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
                  ClaimsPrincipal principal,
                  ILogger log)
              {
                  return new OkObjectResult(new
                  {
                      Name = principal.Identity.Name,
                      IsAuthenticated = principal.Identity.IsAuthenticated,
                      Claims = principal.Claims.Select(x => new { Type = x.Type, Value = x.Value }).ToArray(),
                  });
              }
          }
      }
      
    2. fun-okazukiauth1 にデプロイ
  4. Function App への Easy Auth の構成
    1. 認証/承認 で App Service 認証をオンにする
    2. 要求が認証されない場合に実行するアクションを Azure Active Directory でのログインにする
    3. Azure Active Directory の構成を行う
      1. 管理モードを簡易に設定
      2. 新しい AD アプリを作成するを選択
      3. アプリ名とその他のオプションはデフォルトのまま作成して保存
  5. 認証のテスト
    1. ポータルから関数をテストして 401 でアクセスできないことを確認
    2. 適当なクライアントアプリから呼び出せるかテスト
      1. WPF アプリを作成
      2. Microsoft.Identity.Client を追加
      3. AAD にクライアント用のアプリを登録
      4. クライアントアプリ側に API アクセス許可を追加しておきます
      5. 管理者の同意を与えるボタンを押しておきます。アプリからのサインイン時に確認画面が出るのがダルイので
      6. WPF アプリにボタンとテキストボックスを置いてボタンクリックイベントを以下のようにべたっと書きます
        using Microsoft.Identity.Client;
        using System;
        using System.Linq;
        using System.Net.Http;
        using System.Net.Http.Headers;
        using System.Windows;
        
        namespace WpfApp1
        {
            /// <summary>
            /// Interaction logic for MainWindow.xaml
            /// </summary>
            public partial class MainWindow : Window
            {
                private const string Endpoint = "https://関数アプリ名.azurewebsites.net/api/Function1";
                private const string Scope = "https://関数アプリ名.azurewebsites.net/user_impersonation";
                private HttpClient _client = new HttpClient();
                private IPublicClientApplication _pca;
                public MainWindow()
                {
                    InitializeComponent();
                    _pca = PublicClientApplicationBuilder.Create("テスト用クライアントアプリのクライアントID")
                        .WithRedirectUri("http://localhost")
                        .WithAuthority("https://login.microsoftonline.com/Azure AD のテナント ID/")
                        .Build();
                }
        
                private async void Button_Click(object sender, RoutedEventArgs e)
                {
                    try
                    {
                        AuthenticationResult result;
                        try
                        {
                            var accounts = await _pca.GetAccountsAsync();
                            result = await _pca.AcquireTokenSilent(new[] { Scope }, accounts.FirstOrDefault())
                                .ExecuteAsync();
                        }
                        catch (MsalUiRequiredException)
                        {
                            result = await _pca.AcquireTokenInteractive(new[] { Scope }).ExecuteAsync();
                        }
        
                        var req = new HttpRequestMessage(HttpMethod.Get, Endpoint);
                        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
                        var res = await _client.SendAsync(req);
                        res.EnsureSuccessStatusCode();
                        textBox.Text = await res.Content.ReadAsStringAsync();
                    }
                    catch (Exception ex)
                    {
                        textBox.Text = ex.ToString();
                    }
                }
            }
        }
        
      7. ボタンを押すと以下のような JOSN が返ってきました。ばっちり。
        {
            "name": "k_ota28@hotmail.com",
            "isAuthenticated": true,
            "claims": [
                長いので省略
            ]
        }
        
  6. API Management で保護
    1. Azure Functions の API Management から API Management に API を定義してもらいましょう
    2. インポートできたら API Management の該当関数アプリの All operations の Inbound に validate-jwt タグを追加します
      <policies>
          <inbound>
              <base />
              <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
                  <openid-config url="https://login.microsoftonline.com/Azure AD のテナント ID/.well-known/openid-configuration" />
                  <required-claims>
                      <claim name="aud" match="all">
                          <value>https://fun-okazukiauth1.azurewebsites.net</value> <!-- 裏側の function app のアプリ ID URL -->
                      </claim>
                  </required-claims>
              </validate-jwt>
          </inbound>
          <backend>
              <base />
          </backend>
          <outbound>
              <base />
          </outbound>
          <on-error>
              <base />
          </on-error>
      </policies>
      
    3. 今回は Subscription Key 無しで簡単にテストしたいので API Management の Settings で Subscription required のチェックを外します
    4. この時点でテスト用クライアントからよびだす URL を API Management のエンドポイントに変えると API Management を経由して呼び出し可能になります