しばやん雑記

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

Azure AD B2C のカスタムポリシー利用に必要なアプリケーションを Terraform を使って登録する

最近の Azure AD B2C は組み込みユーザーフローの機能がかなり増えているので、ある程度のことなら Azure Portal からの設定で実現できますが、結局は IEF を使ったカスタムポリシーが必要になるケースも多いです。

Azure AD B2C のカスタムポリシーは触ったことがある人ならわかると思いますが、XML ベースの難解な定義の集合体になっています。確実にバージョン管理と CI/CD を行っておきたいですが、Azure Portal からアップロードする運用になっていることも多いのではないかと思います。

将来的にそのあたりを Terraform Provider for Azure AD が綺麗に解決してくれそうなので、今回はカスタムポリシーを利用する際に必要な IdentityExperienceFrameworkProxyIdentityExperienceFramework という 2 つのアプリケーションを登録し、自動化の準備をしておきます。

カスタムポリシーを使う上で必要になるのでセットアップツールも用意されていますが、全体を Terraform で管理することを踏まえると上手くモジュール化し、再利用可能な形にしておくのが正解だと思います。

IEF 用アプリケーションと言っても、登録方法は通常のアプリケーションとほぼ同じです。前回書いたエントリでもほぼ同じことをしているので参照してください。

単純に Azure AD B2C を利用するアプリケーションとは異なり、API の公開とその API に対する許可の付与が絡み合っているので、Azure Portal で作業する場合は結構ミスが多くなります。

Terraform 化すると Application ID を反対にしてしまうといったありがちなミスを減らすことが出来ます。

共通で利用するリソースを定義

まずは 2 つのアプリケーションの両方で利用するリソースを定義しておきます。

Microsoft Graph のサービスプリンシパルは毎回使うのでお馴染みですが、今回は Azure AD B2C のテナント名をローカル変数として用意しておきます。

terraform {
  required_providers {
    azuread = "~> 2.0"
  }
}

data "azuread_domains" "default" {
  only_initial = true
}

data "azuread_application_published_app_ids" "well_known" {}

data "azuread_service_principal" "msgraph" {
  application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
}

locals {
  domain_name = trimsuffix(data.azuread_domains.default.domains.0.domain_name, ".onmicrosoft.com")
}

本来ならテナント名が azuread_domains のプロパティで取れると楽なのですが、ドメイン名という形でのみ取得可能なので trimsuffix を使ってテナント名を切り出しています。

確認はしていないのですが、カスタムドメインを当てた場合には挙動が変わる可能性もあります。

IdentityExperienceFramework を定義

2 つのアプリケーション間には API の依存関係が存在するため、先に IdentityExperienceFramework から定義していきます。こちらは API を公開する側になります。

注意点としては sign_in_audienceAzureADMyOrg を指定することと、identifier_uris に指定する値です。Azure Portal から設定する場合は Application ID が使われますが、Terraform では事前にテナント内でユニークな値を指定する必要があります。今回は適当な値を指定しています。

resource "random_uuid" "user_impersonation" {}

resource "azuread_application" "identity_experience_framework" {
  display_name     = "IdentityExperienceFramework"
  sign_in_audience = "AzureADMyOrg"
  identifier_uris = ["https://${local.domain_name}.onmicrosoft.com/identity-experience-framework"]

  api {
    requested_access_token_version = 2

    oauth2_permission_scope {
      admin_consent_description  = "Allow the application to access IdentityExperienceFramework on behalf of the signed-in user."
      admin_consent_display_name = "Access IdentityExperienceFramework"
      enabled                    = true
      id                         = random_uuid.user_impersonation.result
      type                       = "User"
      value                      = "user_impersonation"
    }
  }

  required_resource_access {
    resource_app_id = data.azuread_service_principal.msgraph.application_id

    resource_access {
      id   = data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"]
      type = "Scope"
    }

    resource_access {
      id   = data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["offline_access"]
      type = "Scope"
    }
  }

  web {
    redirect_uris = ["https://${local.domain_name}.b2clogin.com/${local.domain_name}.onmicrosoft.com"]
  }
}

resource "azuread_service_principal" "identity_experience_framework" {
  application_id = azuread_application.identity_experience_framework.application_id
}

resource "azuread_service_principal_delegated_permission_grant" "identity_experience_framework" {
  service_principal_object_id          = azuread_service_principal.identity_experience_framework.object_id
  resource_service_principal_object_id = data.azuread_service_principal.msgraph.object_id
  claim_values                         = ["openid", "offline_access"]
}

それ以外は前回作成した Azure AD B2C のアプリケーションとほぼ変わりません。

redirect_uris はチュートリアルで指定されたフォーマットで指定すれば良いです。実際のところ URI であればなんでも良さそうですが、ドキュメントの通りに従っておくことにします。

この定義を使って terraform apply を実行すると、以下のようにアプリケーションが作成されます。

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

必要な API が公開されていることや、管理者の許可が付与されていることも確認出来るはずです。

ProxyIdentityExperienceFramework を定義

次に API を利用する側となる ProxyIdentityExperienceFramework を定義していきます。こちらはパブリッククライアントとして定義するので、これまでとは少し違います。

ポイントは fallback_public_client_enabledtrue を指定するのと、public_client ブロックを追加して redirect_urismyapp://auth を指定することです。

resource "azuread_application" "proxy_identity_experience_framework" {
  display_name                   = "ProxyIdentityExperienceFramework"
  sign_in_audience               = "AzureADMyOrg"
  fallback_public_client_enabled = true

  required_resource_access {
    resource_app_id = data.azuread_service_principal.msgraph.application_id

    resource_access {
      id   = data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"]
      type = "Scope"
    }

    resource_access {
      id   = data.azuread_service_principal.msgraph.oauth2_permission_scope_ids["offline_access"]
      type = "Scope"
    }
  }

  required_resource_access {
    resource_app_id = azuread_application.identity_experience_framework.application_id

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

  public_client {
    redirect_uris = ["myapp://auth"]
  }
}

resource "azuread_service_principal" "proxy_identity_experience_framework" {
  application_id = azuread_application.proxy_identity_experience_framework.application_id
}

resource "azuread_service_principal_delegated_permission_grant" "proxy_identity_experience_framework_msgraph" {
  service_principal_object_id          = azuread_service_principal.proxy_identity_experience_framework.object_id
  resource_service_principal_object_id = data.azuread_service_principal.msgraph.object_id
  claim_values                         = ["openid", "offline_access"]
}

resource "azuread_service_principal_delegated_permission_grant" "proxy_identity_experience_framework_ief" {
  service_principal_object_id          = azuread_service_principal.proxy_identity_experience_framework.object_id
  resource_service_principal_object_id = azuread_service_principal.identity_experience_framework.object_id
  claim_values                         = ["user_impersonation"]
}

このアプリケーションは IdentityExperienceFramework が公開している API を必要とするので、required_resource_access ブロックと azuread_service_principal_delegated_permission_grant リソースの追加が必要になります。

そしてこの定義に対して terraform apply を実行すると、管理者の許可が付与された状態で作成されます。

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

定義は少し長くなり、リソースも複数出てきますがカスタムポリシーを利用する場合の全てで同じ定義が利用できるので、正確な定義をモジュール化して使いまわしが出来るというメリットは大きいです。

カスタムポリシーから利用する

最後に作成した 2 つのアプリケーションを利用して、実際にカスタムポリシーを作成して動作するか確認しておきます。カスタムポリシー自体はチュートリアルにもある starterpack を使いました。

ポリシーキーの作成は行えないので TokenSigningKeyContainerTokenEncryptionKeyContainer は手動で作成しましたが、将来的には Terraform から作成できるはずです。

アップロード前に starterpack に用意された XML に対して、Application ID とテナント名の修正を行っておきます。正直なところ手動だとミスが起こりがち作業ですが、ポリシーのアップロードに Terraform が対応すれば templatefile 関数を使って、この辺りの修正も自動化出来るようになります。

修正した XML ファイルを Azure Portal からアップロードしていきます。エラーが出なければ問題ないです。

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

設定に問題が無ければ、アップロードした B2C_1A_SIGNUP_SIGNIN を選んでユーザーフローを実行すると、ログインから id_token の取得まで成功するはずです。

割と設定項目が多いので、手動で設定すると大体 1 度ははまるのですが、Terraform を使ってコード化することで確実にセットアップできるようになりました。Azure AD は IaC との相性が凄く良いと感じています。