しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure AD と App Service Authentication を使って Web App と Web API を保護する

Azure AD 認証と App Service Authentication を組み合わせて、Web App へのログインを行いつつ同時に発行されるアクセストークンを利用して、別に用意された API を実行したいというシナリオが存在します。

Microsoft Graph API を呼び出す場合はアプリケーションに許可するスコープを追加すればよいですが、自前で用意した API の場合にはまず API を公開するという設定が必要になります。

公式ドキュメントでも Web App から Web API を呼び出すシナリオが紹介されていますが、Azure AD にアプリケーションを登録するあたりは若干省略されていてわかりにくいです。

最近は Azure AD の設定を Azure Portal からではなく Terraform で全て行うブームが個人的に来ているので、今回も Terraform を使って Azure AD と App Service の設定まで行ってしまいます。

作成した Terraform 定義は長いので GitHub で公開しています。フロントエンドの Web App には App Service を、バックエンドの Web API には Azure Functions を利用して、その両方を Azure AD と App Service Authentication を使って保護しています。

実際に触ってみると Azure Portal はアプリケーションを登録する際に、色々と追加の設定を行っていることが分かります。Terraform を使った場合はそういった設定は行わないので、全て明示的に設定する必要がありますが、個人的には逆に理解がしやすかったです。

ここから先はサンプル定義の Azure AD のアプリケーション設定周りを中心に説明をしておきます。

Web API の Azure AD アプリケーション設定

バックエンドの Web API に対して設定する Azure AD アプリケーションから説明します。この Azure AD アプリケーションは API を公開するので、OAuth 2 のスコープを公開することになります。

Web App ではそのスコープを指定することで、ユーザーに対してアクセス許可の確認を行います。ここで重要なのは identifier_urisoauth2_permission_scope の 2 つになります。

resource "random_uuid" "user_impersonation" {}

resource "azuread_application" "backend" {
  display_name    = "Backend example"
  identifier_uris = ["api://terraform-backend"]
  owners          = [data.azuread_client_config.current.object_id]

  api {
    requested_access_token_version = 2

    # user_impersonation スコープを公開する
    oauth2_permission_scope {
      admin_consent_description  = "Allow the application to access Backend example on behalf of the signed-in user."
      admin_consent_display_name = "Access Backend example"
      enabled                    = true
      id                         = random_uuid.user_impersonation.result
      type                       = "User"
      user_consent_description   = "Allow the application to access Backend example on your behalf."
      user_consent_display_name  = "Access Backend example"
      value                      = "user_impersonation"
    }
  }
}

# Application ID に紐づいた Service Principal は必須
resource "azuread_service_principal" "backend" {
  application_id = azuread_application.backend.application_id
}

今回のように Azure AD アプリケーションでスコープを公開する場合は、テナント内でユニークな Uri が必要になります。Azure Portal から設定する場合は大体 api://<application_id> という形式になりますが、Terraform の場合は作成時に Application ID が決まらないので、独自にユニークな文字列を振るのが安全です。

Azure AD のアプリケーション登録周りは用語が色々出てきて分かりにくいので、まずは以下の記事を一通り読んでおくことをお勧めします。実際 Enterprise Application とか名前からは意味が分からないと思います。

Azure Portal を使ってアプリケーションを作成すると user_impersonation と Service Principal は自動的に作成されますが、Terraform ではこのように明示的に定義する必要があります。

この定義を使ってリソースを実際に作成すると、以下のように Azure Portal から Application ID とスコープ付きで作成されているのが確認できます。

f:id:shiba-yan:20220224211345p:plain

アプリケーションと同時に Service Principal も作成されていますが、スクリーンショットは省略します。

バックエンドの Web API 向け Azure AD アプリケーションの設定はこれで終わりになるので、次はフロントエンドの Azure AD アプリケーションの設定をしていきます。

Web App の Azure AD アプリケーション設定

バックエンドに比べるとフロントエンドの Web App 向け設定は、以下のように許可するスコープとして先ほど作成した user_impersonation を追加するだけなのでシンプルです。

追加するにはアクセス先の Application ID とスコープの ID が必要になるので、モジュール化する際には上手く値を渡せるようにする必要があります。

resource "azuread_application" "frontend" {
  display_name = "Frontend example"
  owners       = [data.azuread_client_config.current.object_id]

  required_resource_access {
    resource_app_id = azuread_application.backend.application_id

    resource_access {
      id   = azuread_application.backend.oauth2_permission_scope_ids["user_impersonation"]
      type = "Scope"
    }
  }

  web {
    redirect_uris = ["https://***.azurewebsites.net/.auth/login/aad/callback"]

    implicit_grant {
      access_token_issuance_enabled = false
      id_token_issuance_enabled     = true
    }
  }
}

Web App では実際にユーザーにログインさせる必要があるので web ブロックを追加して、リダイレクト URI などの設定を追加しています。

この定義を使ってリソースを作成すると、バックエンドの API permissions が追加された状態になります。

f:id:shiba-yan:20220224211356p:plain

たったこれだけでフロントエンドの Web App 向け設定は完了です。最後に App Service Authentication の設定を修正して、バックエンドに向けのアクセストークンを取得するようにすれば完了です。

App Service Authentication から利用する

App Service Authentication の修正が必要になるのは、実際にユーザーがログインするフロントエンド Web App です。具体的には additional_login_params で今回作成したスコープを追加するだけです。

resource "azurerm_app_service" "frontend" {
  name                       = "app-terraform-frontend"
  # 本題と関係のないプロパティは省略

  auth_settings {
    enabled                       = true
    token_store_enabled           = true
    default_provider              = "AzureActiveDirectory"
    unauthenticated_client_action = "RedirectToLoginPage"
    issuer                        = "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0"

    # バックエンド向けのアクセストークンを要求する
    additional_login_params = {
      "scope" = "openid profile email offline_access api://terraform-backend/user_impersonation"
    }

    active_directory {
      client_id     = azuread_application.frontend.application_id
      client_secret = azuread_application_password.frontend.value
    }
  }
}

この修正をデプロイすると以下のようにログイン時に表示されるアクセス許可の確認画面で、バックエンド Web API へのアクセス許可を与えるかを追加で聞かれるようになります。

f:id:shiba-yan:20220224202256p:plain

承諾してログイン処理を完了すると、アクセストークンを App Service Authentication の /.auth/me エンドポイントや、サーバーサイドの場合は HTTP リクエストヘッダーから取得できるようになります。

以下では実際に /.auth/me エンドポイントを叩いてアクセストークンを確認しています。

f:id:shiba-yan:20220224203343p:plain

本来であればフロントエンドのアプリケーションからバックエンドの Web API を叩くのですが、今回はアクセストークンを使って Postman でバックエンドの Web API を呼び出せるか確認します。

まずは Bearer Token 無しでリクエストを投げた場合です。401 が返されてリクエストは通りません。

f:id:shiba-yan:20220224203654p:plain

次に /.auth/me エンドポイントを叩いて取得したアクセストークンを付けて呼び出してみます。今度は問題なくバックエンドにデプロイした Azure Functions が実行されていることが確認できます。

f:id:shiba-yan:20220224203703p:plain

複数アプリケーションが関係するという Azure Portal だと分かりにくかった設定が、全て Terraform で管理することでそれぞれの依存関係が明確になり、圧倒的に分かりやすくなったと感じています。

若干 Service Principal の辺りがハマりがちなポイントにはなりますが、基本的にセットで作ると考えておけば問題はないです。もうちょっと Terraform Provider for Azure AD ブームは続きます。