こども端末の引き継ぎと createCustomToken の IAM 落とし穴
「ぽんっ!」(旧称:でいりんぐ)はこどものスタンプや育成中のキャラを守るため、機種変更時の端末引き継ぎを用意しています。旧端末で承認すると、新端末が自動でサインインしてデータを引き継ぐ流れです。この機能を本番に出したところ、承認したのに新端末が「承認待ち」のまま固まるという不具合に遭遇しました。
背景(当時の状況)
端末引き継ぎは、機種変更でこどものスタンプや育成キャラを失わないための機能で、Cloud Functions とルールを本番へデプロイして実機で試したのが 2026-06-23 でした。ところが、旧端末で承認したあと新端末が「承認待ち」のまま進まない。ローカルのエミュレータでは通っていたので、本番でだけ表面化するタイプだと分かるまで少し時間がかかりました。関数を第2世代(gen2)で動かしていたことが、この権限問題の伏線になっていました。
引き継ぎの流れ
大まかには次のような構成です。
- 新端末が引き継ぎリクエストを作成する。
- 旧端末(こども本人)がリクエストを承認する。
- サーバ(Cloud Functions)が承認を受けて カスタムトークンを発行し、新端末はそのトークンでサインインして同じアカウントに入る。
この 3 の「カスタムトークン発行」で admin.auth().createCustomToken() を呼んでいます。
症状: 承認しても新端末が固まる
実機で試すと、旧端末の承認までは通るのに、新端末が待機画面から進まない。ログを追うと、completeHandover 関数が次のエラーで落ちていました。
auth/insufficient-permission:
Permission 'iam.serviceAccounts.signBlob' denied
根本原因: gen2 の実行サービスアカウントに署名権限がない
createCustomToken() は、内部でサービスアカウントの秘密鍵を使わずに IAM の signBlob API を叩いてトークンに署名します。ここで署名権限が必要になります。
Cloud Functions 第2世代(gen2) の関数は、既定で Compute Engine の既定サービスアカウントを実行 ID として動きます。この SA には iam.serviceAccounts.signBlob 権限が付いていないため、createCustomToken() が権限不足で失敗していました。ローカルのエミュレータでは Admin SDK が別経路で署名できてしまい、本番で初めて表面化するタイプの罠です。
対処: 実行 SA に Token Creator ロールを付与
実行サービスアカウント自身に、自分自身へのトークン発行者ロールを付けて解決しました。
gcloud iam service-accounts add-iam-policy-binding \
<PROJECT_NUMBER>-compute@developer.gserviceaccount.com \
--member="serviceAccount:<PROJECT_NUMBER>-compute@developer.gserviceaccount.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project="<YOUR_PROJECT_ID>"
createCustomToken() を使う関数を今後追加するときも、同じ署名権限が必要になります。gen2 に移行した関数がある場合は特に注意が必要です。
設計上の反省: 待機画面がエラーを握りつぶしていた
この不具合を悪化させた要因がもう一つありました。新端末の承認待ち画面が、サーバ側の想定外エラーを握りつぶして黙って待機を続けていたのです。そのため IAM の権限エラーがユーザー画面にもログにも出てこず、「なぜか固まる」という切り分けにくい症状になっていました。
- ポーリング系の待機画面は、一定回数失敗したらエラーを表示して抜けるべき。
- 「成功するまで待つ」実装は、失敗が可視化されず調査を遅らせます。
まとめ
- gen2 の Cloud Functions で
createCustomToken()を使うなら、実行サービスアカウントにroles/iam.serviceAccountTokenCreatorが要る。ローカルでは通っても本番でsignBlob deniedになる定番の落とし穴。 - 待機・ポーリング画面はエラーを握りつぶさない。想定外の失敗を一定回数で表面化させると、こうした本番限定の不具合の調査が段違いに速くなります。