Workforce Identity FederationでEntra IDと連携しサインインまで通す

アンバーの門をくぐる人のイラスト。左に ID カードが詰まった紺色のキャビネット、中央の門の上に鍵と緑のチェック、右にダッシュボードのブラウザー画面、右下に赤い斜線の入ったカードが置かれている。画像内の文字は Workforce Identity Federation、No Google Account Needed、Entra ID、Workforce Pool、Console Access 認証・IAM

はじめに

Workforce Identity Federation を使うと、Entra ID のユーザーが、Google アカウントを持たないまま Google Cloud コンソールにサインインできます。Entra ID は Microsoft が提供する ID 管理サービスで、Azure や Microsoft 365 のアカウントを管理しています。

本記事では、Entra ID を IdP として Workforce Identity Pool に接続し、実際にサインインするまでの手順を紹介します。IdP(Identity Provider)は、ユーザーの ID を管理してログインを受け持つサービスです。設定は Entra 側も Google Cloud 側も Terraform で管理しました。

Workforce Identity Federation の概念と Pool の作り方は、Workforce Identity Federationとは?Workloadとの違いで解説しています。本記事はその続きです。

本記事のまとめ

  • Entra 側のリダイレクト URI に Pool ID と Provider ID が入るため、ID を先に決めてから両側を設定する。
  • OIDC は認可コードフローを選ぶ。implicit フローにすると、後から使える機能が減る。
  • client secret は Terraform の外で発行し、write-only 引数で渡すと、Azure と Google Cloud のどちらの state にも平文が残らない。
  • subject に別の ID を入れてもサインインは成功してしまう。値の出どころまで確認する。
Google Cloud のコンソール(連携)の画面。「このページは、Workforce Identity 連携ユーザー向けに更新されました」というバナーが表示されている
Entra ID のユーザーでサインインした直後の画面。「Workforce Identity 連携ユーザー向けに更新されました」というバナーが出ている

Entra ID と Workforce Identity Federation で連携する全体の流れ

作業は Entra 側と Google Cloud 側を行き来します。Entra 側のアプリ登録に Pool ID と Provider ID を含む URL を書くため、2 つの ID を最初に決めておくのが要点です。

  1. Google Cloud で Workforce Identity Pool を作り、Pool ID と Provider ID を決める
  2. Entra ID でアプリを登録し、リダイレクト URI を設定する
  3. Entra ID で client secret を発行する
  4. Google Cloud で Pool に OIDC の Provider を作る
  5. Google Cloud の IAM で Entra のユーザーにロールを付け、サインインする

OIDC(OpenID Connect)は、ログインした人が誰かを、IdP からアプリへ安全に伝えるための仕組みです。Workforce Identity Federation は OIDC と SAML 2.0 に対応しており、私は Entra 側の設定が少ない OIDC を選びました。

Entra 管理センターのアプリ登録の概要画面。表示名が gcp-workforce-identity-dev で、クライアントの資格情報が「証明書またはシークレットの追加」というリンクだけになっている
Terraform で作成したアプリ登録。クライアントの資格情報が追加リンクのままで、client secret がまだ 1 件も無いことが分かる

Entra ID でアプリを登録する

Entra 側で作るのは、アプリ登録とエンタープライズアプリケーションの 2 つです。リダイレクト URI はプラットフォームを「Web」にして登録します。SPA(シングルページアプリケーション)として登録すると、認可コードフローが通りません。

リダイレクト URI は、Entra でのログイン後に戻る先の URL です。形式は次のとおりで、Pool ID と Provider ID が入ります。

https://auth.cloud.google/signin-callback/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID

私は Entra 側を azuread provider で Terraform 管理しました。主な定義は次のとおりです。

resource "azuread_application_registration" "gcp_wif" {
  display_name     = "gcp-workforce-identity-dev"
  sign_in_audience = "AzureADMyOrg" # 自分のテナントのユーザーだけ

  # グループ単位で権限を付けるときに使う
  group_membership_claims = ["SecurityGroup"]
}

resource "azuread_application_redirect_uris" "gcp_wif" {
  application_id = azuread_application_registration.gcp_wif.id
  type           = "Web"
  redirect_uris  = [local.gcp_redirect_uri]
}

# エンタープライズアプリケーション。ユーザーのサインインに必要
resource "azuread_service_principal" "gcp_wif" {
  client_id = azuread_application_registration.gcp_wif.client_id
}

アプリ登録には 2 つの書き方があります。1 つのリソースにまとめて書く azuread_application と、設定ごとに分かれたリソース(azuread_application_registration など)です。azuread provider のドキュメントには、分かれたリソースの一部が azuread_application と併用できないと書かれています。後から移すと state の付け替えが要るため、私は最初から分かれたリソースで書きました。

なお、Entra 管理センターの新しい画面では、アプリ登録の「認証」が「リダイレクト URI の構成」「Supported accounts」「設定」の 3 つのタブに分かれていました。暗黙的な許可のチェックは「設定」タブにあります。

アプリ登録の「認証」→「リダイレクト URI の構成」タブ。プラットフォームの種類が Web で、リダイレクト URI の Pool ID が <POOL_ID> に伏せてある
リダイレクト URI はプラットフォームを Web にして登録する。URI には Pool ID と Provider ID が入る。Pool ID は <POOL_ID> に伏せた

OIDC は認可コードフローを選ぶ

Provider の OIDC には、認可コードフロー(CODE)と implicit フロー(ID_TOKEN)の 2 つがあります。私は最初 implicit フローで構築し、レビューを受けて認可コードフローに切り替えました。

認可コードフローでは、トークンがブラウザを通らず、IdP から Google Cloud へ直接渡ります。公式ドキュメントでも、こちらが最も安全な方式とされています。切り替えた理由は、Terraform の Provider リソースのドキュメントとスキーマで確かめた次の 2 点です。

  • スキーマの説明に、implicit フローを避けるため CODE を推奨すると書かれている。
  • ID_TOKEN を選ぶと、クレームの扱いが「ID トークンのクレームだけ」に固定される。userinfo エンドポイントのクレームを合わせて使う設定(MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS)が使えなくなる。

認可コードフローには client secret が必要です。証明書やフェデレーション資格情報で代わりにできないかも調べましたが、Provider の oidc ブロックが受け付けるのは client secret だけでした。フェデレーション資格情報は、Entra が外部のトークンを受け取る側になる仕組みで、今回とは向きが逆です。

CORS の設定も要りませんでした。CORS は、ブラウザ上のスクリプトが別のドメインにアクセスしてよいかを決める仕組みです。この構成でブラウザが行うのは画面の遷移だけで、トークンのやり取りは Google のサーバーと Entra の間で行われます。実際に、CORS に関する設定を何もしないままサインインできました。

SAML 2.0 を選ぶ場合は、Terraform で書けない部分が残ります。調べた時点では、azuread provider に Federation Metadata XML を取得する手段が無く、署名証明書をアクティブにする設定もありませんでした。この 2 つは手作業になります。

アプリ登録の「認証」→「設定」タブ。暗黙的な許可で「ID トークン」にチェックが入り、「アクセス トークン」は未チェックになっている
認可コードフローへ切り替える前の状態。暗黙的な許可の「ID トークン」にチェックが入っている

client secret を state に残さずに渡す

client secret は、Terraform の state に平文を残さない形で渡しました。Entra 側は Terraform の外で発行します。Google Cloud 側は write-only 引数で受け取ります。write-only 引数は、Terraform に渡しても state や plan の出力に値が残らない引数です。

  • Entra 側: azuread_application_password で作ると、値が Azure 側の state に平文で残る。そのため Terraform では作らず、Entra 管理センターで 1 回だけ発行した。有効期限は 6 か月である。
  • Google Cloud 側: Provider の plain_text_wo で受け取る。google provider 7.38.0 以降と Terraform 1.11 以降が必要で、私は provider を 5 系から 7 系に上げた。

apply は CI で実行するため、値は GitHub Secrets に登録し、環境変数 TF_VAR_... として Terraform に渡しています。apply 後に Provider を gcloud で確認すると、client secret は指紋(thumbprint)しか返りませんでした。出力は必要な項目だけを抜き出しています。

$ gcloud iam workforce-pools providers describe PROVIDER_ID \
    --workforce-pool=POOL_ID --location=global
state: ACTIVE
attributeMapping: { google.subject: assertion.oid }
oidc.webSsoConfig: { responseType: CODE, assertionClaimsBehavior: MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS }
oidc.clientSecret.value: { thumbprint: ... }

注意点は失効です。state に値が無いので、有効期限が切れても Terraform は何も警告しません。失効はサインインの失敗としてだけ現れます。次に更新するときは az ad app credential reset --append を使う予定です。--append を付けないと、使用中の secret がすぐに失効します。

アプリ登録の「証明書とシークレット」画面。証明書 (0)、クライアント シークレット (0)、フェデレーション資格情報 (0) で、どれも 0 件
client secret を発行する前の状態。3 つのタブがすべて 0 件で、Terraform ではシークレットを作っていないことが分かる

Workforce Identity Pool に Entra ID の Provider を作る

Provider には、Entra の issuer、client ID、client secret、認可コードフローの設定を書きます。属性マッピングは、まず google.subject = assertion.oid の 1 つだけにしました。

resource "google_iam_workforce_pool_provider" "entra_id" {
  workforce_pool_id = google_iam_workforce_pool.entra_poc.workforce_pool_id
  location          = "global"
  provider_id       = "entra-id"

  attribute_mapping = {
    "google.subject" = "assertion.oid"
  }

  oidc {
    issuer_uri = "https://login.microsoftonline.com/${var.entra_tenant_id}/v2.0"
    client_id  = var.entra_client_id

    client_secret {
      value {
        plain_text_wo         = var.entra_client_secret
        plain_text_wo_version = var.entra_client_secret_version
      }
    }

    web_sso_config {
      response_type             = "CODE"
      assertion_claims_behavior = "MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS"
    }
  }
}

oid は、Entra のテナント内でユーザーごとに変わらないオブジェクト ID です。メールアドレスは変わることがあるので subject には使いません。表示名を出す google.display_name は、クレームが欠けるとサインイン自体が失敗するため、疎通を確認してから足すことにしました。

issuer の値は、認証なしで取得できる Entra の設定情報で裏を取りました。テナントのドメイン名から、Terraform で組み立てた issuer_uri と同じ値が返ります。

$ curl -s https://login.microsoftonline.com/TENANT_DOMAIN/v2.0/.well-known/openid-configuration
issuer = https://login.microsoftonline.com/TENANT_ID/v2.0

属性マッピングには上限があります。前述の公式ドキュメントによると、合計 16 KB までで、独自の属性ルールは 50 個までです。グループを使う場合は、1 ユーザーが 400 を超えるグループに所属するとサインインに失敗します。Entra 側にも上限があり、Microsoft のドキュメントでは、トークンに載るグループは JWT で 200、SAML で 150 までとされています。

IAM でロールを付けてサインインする

最後に、Entra のユーザーに IAM でロールを付けます。サインインの確認が目的なので、付けたのはプロジェクトの一覧を見るだけの roles/browser 1 つです。

roles/browser -> principal://iam.googleapis.com/locations/global/workforcePools/POOL_ID/subject/USER_OBJECT_ID

検証用のユーザーは Entra 管理センターで手作成しました。Terraform の azuread_user で作ると、初期パスワードが state に平文で残るためです。サインインは次の URL から行います。

https://auth.cloud.google/signin/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID?continueUrl=https://console.cloud.google/

サインインすると、通常のコンソールではなく「コンソール(連携)」が開きました。アカウントの表示は Entra の oid と Pool 名で、IAM に書いた principal と一致します。roles/browser だけでプロジェクトを選べることも確認できました。

一方、画面下部には、プロジェクトが「組織なし」と表示されました。実際にはフォルダの下にあるプロジェクトです。roles/browser をプロジェクトにだけ付けていて組織の階層を読めないため、こう表示されたと見られます。

ユーザーのオブジェクト ID を取り違えやすい

principal に入れるのは、Entra の「ユーザー」画面にあるオブジェクト ID です。アプリ登録の概要画面にも「オブジェクト ID」があり、私は最初こちらを渡されていました。

間違えても apply は通り、サインインも成功します。Google Cloud 側は、subject の値が実在するユーザーかを確かめないためです。結果として「ログインできるのにプロジェクトが見えない」という、原因の分かりにくい状態になります。値をどの画面から取ったかまで確認してください。

コンソール(連携)のアカウントメニューに Entra のオブジェクト ID と Pool のパスが表示され、プロジェクトセレクタにプロジェクトが出ている画面。オブジェクト ID・Pool ID・プロジェクト ID は伏せてある
サインイン後のコンソール(連携)。アカウント表示が Entra の oid と Pool 名になり、下部には「組織なし」と出ている。oid は <ENTRA-USER-OBJECT-ID>、Pool ID は <POOL_ID>、プロジェクト ID は <PROJECT-ID> に伏せた

まとめ

Entra ID のユーザーを、Google アカウントを作らずに Google Cloud コンソールへサインインさせることができました。設定は Entra 側と Google Cloud 側の両方を Terraform で管理しています。

  • Pool ID と Provider ID を先に決め、Entra のリダイレクト URI(プラットフォームは Web)と一致させる。
  • OIDC は認可コードフローにし、client secret は Terraform の外で発行して write-only 引数で渡す。失効は警告なしにサインインの失敗として現れるので、有効期限を管理する。
  • subject には Entra のユーザーの oid を使い、どの画面から取った値かまで確認する。

なお、管理画面や provider の仕様、上限値は変わります。実際に設定するときは、最新を公式ドキュメントで確認してください。

次は、Entra ID のユーザーに IAP で Cloud Run のアプリを見せる構成を試します。グループ単位の権限もそこで扱います。