自分の昔の記憶(2022年頃)ではVercelから Google Cloudへの認証に Workload Identity Federation が使えず、Service Account の秘密鍵をVercelの環境変数に登録する必要があり微妙だなーと思っていました。 久しぶりに調べてみたところ Vercel OIDC がサポートされており、秘密鍵なしでフェデレーションできるようになっていました。
例えば、Next.js のサーバー上で Firestore へアクセスするみたいなユースケースが死ぬほどあります(個人的にあんまり好きではない)が、雑に調べるとみんな Service Account の 秘密鍵を環境変数に登録して〜みたいなことやってます。鍵の漏洩リスクやローテーション管理を考えると、そんなことしないほうが良いです。
とりあえず使ってみる
Workload Identity Federation は、外部 IdP のトークンを STS で検証し一時的な認証情報と交換する仕組みです。Service Account impersonation で外部ワークロードがService Accountの権限を使いGoogle Cloud リソースにアクセスできます。
Vercel OIDC の場合、Vercel が発行したOIDC トークン → STS で検証 → SA を借用、という流れです。図にすると↓

このフローでは google-cloud-node-core の ExternalAccountClient を使い、subject_token_supplier に getVercelOidcToken を動的に渡します。
Firestoreのアクセスで使ってみる
Provider と SA を用意します。
resource "google_iam_workload_identity_pool" "vercel" { workload_identity_pool_id = "vercel-pool" display_name = "Vercel Pool" } resource "google_iam_workload_identity_pool_provider" "vercel" { workload_identity_pool_id = google_iam_workload_identity_pool.vercel.workload_identity_pool_id workload_identity_pool_provider_id = "vercel-provider" display_name = "Vercel Provider" oidc { issuer_uri = "https://oidc.vercel.com/${var.vercel_team_slug}" } attribute_mapping = { "google.subject" = "assertion.sub" "attribute.sub" = "assertion.sub" } attribute_condition = "assertion.sub == 'owner:${var.vercel_team_slug}:project:${var.vercel_project_id}:environment:${each.value}'" } resource "google_service_account" "vercel_app" { account_id = "app" display_name = "Service Account" } resource "google_project_iam_member" "firestore_user" { project = var.project_id role = "roles/datastore.user" member = "serviceAccount:${google_service_account.vercel_app.email}" } resource "google_service_account_iam_member" "vercel_impersonation" { for_each = toset(var.vercel_environments) service_account_id = google_service_account.vercel_app.name role = "roles/iam.workloadIdentityUser" member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.vercel.name}/attribute.sub/owner:${var.vercel_team_slug}:project:${var.vercel_project_id}:environment:${each.value}" } resource "google_service_account_iam_member" "vercel_token_creator" { for_each = toset(var.vercel_environments) service_account_id = google_service_account.vercel_app.name role = "roles/iam.serviceAccountTokenCreator" member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.vercel.name}/attribute.sub/owner:${var.vercel_team_slug}:project:${var.vercel_project_id}:environment:${each.value}" }
attribute_condition でproject + environmentを絞りましょう。これがないと同じ team 内の別プロジェクトからもアクセスできてしまいます。
次にアプリケーション側。後述する理由でfirebase-adminは使わず、@google-cloud/firestore にauthClientを直接渡します。
import { Firestore } from "@google-cloud/firestore"; import { ExternalAccountClient } from "google-auth-library"; import { getVercelOidcToken } from "@vercel/oidc"; const authClient = ExternalAccountClient.fromJSON({ type: "external_account", audience: `//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}`, subject_token_type: "urn:ietf:params:oauth:token-type:jwt", token_url: "https://sts.googleapis.com/v1/token", service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateAccessToken`, subject_token_supplier: { getSubjectToken: getVercelOidcToken, }, }); const firestore = new Firestore({ projectId: "my-project", databaseId: "my-database", authClient, });
これだけでService Accountが発行したトークンを利用してFirestoreをクエリできます。
firebase-adminに関して
firebase-adminのFirestoreモジュール(firebase-admin/firestore)は、内部で@google-cloud/firestoreを初期化する際にcredentialをチェックしています。
firestore-internal.tsのgetFirestoreOptions では:
credential instanceof ServiceAccountCredential→ OKisApplicationDefault(credential)→ OK- それ以外 →
firestore/invalid-credentialエラー
ExternalAccountClient はどちらにも該当しないため、以下のエラーになります。
Failed to initialize Google Cloud Firestore client with the available credentials. Must initialize the SDK with a certificate credential or application default credentials to use Cloud Firestore API.
firebase-admin v13 は external_account 型の credential ファイルをサポートしましたが、Vercel OIDC には使えません。
applicationDefault() の内部経路
ApplicationDefaultCredentialはnew GoogleAuth({ scopes })を呼ぶだけで、authClientやsubject_token_supplierを渡す手段がないGoogleAuthがGOOGLE_APPLICATION_CREDENTIALSから JSON を読み込むtype: "external_account"を検出しExternalAccountClient.fromJSONを呼ぶ- JSON から
IdentityPoolClientを生成する
ここで問題になるのが subject_token_supplier です。これは関数を含むオブジェクトなので JSON にシリアライズできません。applicationDefault() は JSON ファイルからしか credential を読まないため、getVercelOidcToken を渡す方法がないわけです。
No valid Identity Pool "credential_source" provided, must be either file, url, or certificate.
なのでVercel OIDCを使う時は@google-cloud/firestoreを直接使う必要があります。@google-cloud/firestore なら authClient を直接渡せます。経路は面倒なので説明しませんが興味がある人はこの辺を↓を見てください。
まとめ
Firestoreの例で示しましたが、他も同様な感じでOIDCが使えると思います。