最近私は Firebase Authentication を利用したWebアプリケーションを開発しています。Pull Requestごとのプレビュー環境を用意しようとした際に表題の「承認済みドメイン」をどうするかがネックになり色々と調べて最終的にGitHub Actionsで対応したので、そのときわかったことなどをこちらの記事に書きます。
承認済みドメイン (Authorized Domain) とは
Firebase Authenticationを利用すると、「メール・パスワード」「Google認証」「Twitter認証」などなど色々な方式でのアカウント登録周りの認証を実現できます。
アプリケーションをデフォルトの https://{PROJECT_ID}.firebaseapp.com
みたいなドメインではなく、例えば独自ドメインで動かしたい場合はそのドメインからのリクエストが信頼できることを示すために「承認済みドメイン」として管理画面より登録する必要があります。これを登録しないと弾かれて正常にアカウント周りの処理が使えなくなります。
今回の困りごと
Pull Requestごとのプレビュー環境についてはブランチ名をサブドメインとして https://{BRANCH_NAME}.example.com/
という形でアクセスできるようにしましたが、Firebase Authenticationの「承認済みドメイン」にブランチが増えたり減ったりするたびにそのURLを手で追加・削除するのは不便でした。少し調べたところ同じように困ってる人が世間に結構いるようでした。
世間の様子
Firebase AdminのNode.jsのSDKには、この機能がほしいという2019年からOpenなissueがあがっていて、進捗なさそうにみえる。*1
github.com
一方でStackOverflowを眺めてみると、この課題に対処するための案がいくつかありました。例えば、
- 動作確認用のProjectを別に用意し動作確認したいブランチごとにTraffic Migrationを行う方法*2
- 固定でN個の承認済みドメインを追加しておいてPull Requestの番号にたいして剰余演算でいずれかのURLに割り当てられるようにする方法 (つまり
pr-number % N
でサブドメインを決める形) *3 - APIで追加はできないからサポートにFeature Request送りましょうという2020年のコメント *4
- Identity Toolkit APIを使ったらできたよという2022年のコメント *5
Identity Toolkit APIというのを使えば実現できるのであれば、 gcloudというCLIツールを使って上手いことできるのかなと思いましたが、どうやら現状できないようでした。*6
というわけで今回はAPIを叩くことにしました。
作戦
- GitHub Actionsを使う
- 認証はGoogle公式の便利なActionsがあるのでそれで行う
- Pull Requestを開いたときにAPI叩いて承認済みドメインを追加、閉じたときもAPI叩いて承認済みドメインを取り除く
- ひとまず素朴に curl と jq でAPIを叩いたりレスポンスを整形したりする
StackOverflowの回答にリンクされていたブログ記事をかなり参考にしました。
pretired.dazwilkin.com
実装
認証をどうするかと、どのAPIをどのように叩けばいいのかと、GitHub Actionsの実装をどうするかのざっくり3つの話題があるので順に紹介します。
認証周り
今回のAPIを叩くにはFirebase周りの操作ができるService Accountを用意する必要があります。
# Identity Toolkitを有効化 gcloud services enable identitytoolkit.googleapis.com --project=${PROJECT} # Service Accountを作成 gcloud iam service-accounts create ${ACCOUNT} --project=${PROJECT} # 作ったService Accountに firebaseauth.admin 権限付与 gcloud projects add-iam-policy-binding ${PROJECT} --member=serviceAccount:${EMAIL} --role=roles/firebaseauth.admin
このままこのService Accountのキーを取得してGitHubのRepository Secretsに登録するのが昔ながらのやり方ですが、現代ではWorkload identity federation を使うのがおすすめです(Secretに登録せずに済む)。
Workload identity federationの設定の仕方はいろいろと丁寧に書いてあるので以下の gcloud でやるやり方や、Terraformのモジュールなどを参考にすると良いです。
- https://github.com/google-github-actions/auth#setting-up-workload-identity-federation
- terraform-google-github-actions-runners/modules/gh-oidc at master · terraform-google-modules/terraform-google-github-actions-runners · GitHub
また最近だと以下の日本語の記事がわかりやすいです。
eh-career.com
一点気にするポイントとしては、先に作ったService Accountとして操作を行いたいので、 gcloud iam service-accounts add-iam-policy-binding
するときにちゃんとそのアカウントを指定してることを確認しておく必要があります。
# ${SERVICE_ACCOUNT} の部分が、先に用意したサービスアカウントのメールアドレス gcloud iam service-accounts add-iam-policy-binding ${SERVICE_ACCOUNT} \ --project="${PROJECT_ID}" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
APIを叩く
利用するAPIは Identity Platform の getConfig と updateConfig の二つです。
cloud.google.com
cloud.google.com
手元で curl で getConfig を叩きたい場合は以下のように Bearer トークン指定して取得できます。
# $TOKENは gcloud auth print-access-token とかしたときの値を使う curl --header "Authorization: Bearer $TOKEN" \ --header "Accept: application/json" \ "https://identitytoolkit.googleapis.com/v2/projects/${PROJECT}/config"
getConfigの返り値は承認済みドメインに限らず、プロジェクトの管理画面で確認できるようなさまざまな値が返ってきます。この中で承認済みドメインは authorizedDomains
というフィールドに配列として入ってるので jq .authorizedDomains
という風にフィルターすると現在のドメインの登録状況を一覧して確認できます。
$ curl ...(以下略) | jq .authorizedDomains { "authorizedDomains": [ "localhost", "test.example.com", ] }
updateConfigのAPIはPATCHメソッドで叩く形になっていて「更新したい部分のみ指定して更新」します。authorizedDomains
のみ更新したい場合は、?updateMask=authorizedDomains
とクエリパラメータで指定した上で、 --data
に元々の authorizedDomains
と同じ形式のjsonを指定してあげる必要があります。
curl --request PATCH --header "Authorization: Bearer $TOKEN" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data '{"authorizedDomains": ["localhost", "test.example.com", "test2.example.com"]}' \ "https://identitytoolkit.googleapis.com/v2/projects/$PROJECT/config?updateMask=authorizedDomains"
つまり一度現在の設定の配列を取得し、新しく追加・削除したいURLをその配列から追加・削除した上で、更新APIで上書きするという処理が必要です。
jq 技を使う
こちらの記事にも書いてあるやり方を採用しました。つまりcurlとjqでなんとかしました。
jq には Array construction と Object Construction という機能があり、パイプしながら配列やオブジェクトを操作することができます。
stedolan.github.io
例えば配列にたいして jq '.array | . + ["追加したい"]'
みたいにすると "追加したい"
という文字列をおもむろに配列末尾に追加できます。
https://jqplay.org/s/9GY95aZGlVc
同様に配列 +
を -
にすると配列から取り除くことができます。
https://jqplay.org/s/AdTArnJhj7i
これをさらにパイプして jq '.array | {"オブジェクトにしたい": . }'
とすると"オブジェクトにしたい"
をキーとしたオブジェクトにすることができます。
https://jqplay.org/s/S6qEjDtCQSQ
というわけで、getConfig時の結果に jq で今回追加したいURLを追記しつつオブジェクトに変換すれば、updateConfigのAPIに渡すパラメータが出来上がります。
# 現在の設定を取得しつつ、jq を使ってレスポンスを整形 DATA=$(curl --header "Authorization: $TOKEN" \ --header "Accept: application/json" \ "https://identitytoolkit.googleapis.com/v2/projects/$PROJECT/config" | \ jq '.authorizedDomains + ["新しいドメイン"] | {"authorizedDomains": .}') # 上で組み立てたDATA変数をそのまま --data に渡して更新APIを叩いて更新 curl --request PATCH --header "Authorization: Bearer $TOKEN" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data "$DATA" \ "https://identitytoolkit.googleapis.com/v2/projects/$PROJECT/config?updateMask=authorizedDomains"
GitHub Actions
あとはGitHub Actions上で実現する方法ですが、google-github-actions/auth
という公式Actionsを使えば楽です。
READMEの Generating an OAuth 2.0 Access Token
に書いてあるところを参考にActionsを書いていけば curl するときに必要なBearerトークンが使えるようになります。
jobs: job_id: # Workload identity federation 使うときに必要 permissions: contents: 'read' id-token: 'write' steps: - uses: 'actions/checkout@v3' - id: 'auth' name: 'Authenticate to Google Cloud' uses: 'google-github-actions/auth@v0' with: # 後のStepで access_token を使えるようにするのにこの指定が必要 token_format: 'access_token' workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' # ここのservice_accountはfirebaseの権限を持ったものにする service_account: 'my-service-account@my-project.iam.gserviceaccount.com' access_token_lifetime: '300s' # 後のStepで上記のstepのoutputs.access_tokenを使うことができる - id: 'example' run: |- curl --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" https://example.com/
こういう風にすればあとは GitHub Actions 内で普通に curl や jq 使えるので、Pull Request open時やclose時にお好みでAPIを叩けば完成です。
余談ですがもしプレビュー環境をGoogle Cloud Runを活用してデプロイしたい場合、生成されるURLがわからないなんてことがあるかもしれません。その場合は google-github-actions/deploy-cloudrun
GitHub Actionを使うと outputs.url
でURLを取得できて便利かもしれません。
github.com
完成系のGitHub Actions
最終的にこういう感じのyamlになりました。curl周りが初見だとちょっと難しいのが少し気になりますが思ってたより短い行数でできたので一旦許容しています。
name: Update Firebase authorized domain on: pull_request: types: [opened, reopened, closed] env: PROJECT: my-project jobs: update-auth-domain: runs-on: ubuntu-latest permissions: contents: "read" id-token: "write" steps: - name: Checkout uses: actions/checkout@v3 - id: "auth" name: Authenticate Google Cloud uses: "google-github-actions/auth@v0" with: token_format: "access_token" access_token_lifetime: "300s" workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' service_account: "my-service-account@${{ env.PROJECT }}.iam.gserviceaccount.com" - name: Construct preview url id: "construct_url" shell: bash run: | # _ や / を含むブランチ名をハイフンに置換してURLを生成 BRANCH=$(echo ${{github.head_ref}} | sed -e 's/[_/]/-/g') PREVIEW_URL=$BRANCH.example.com echo "##[set-output name=url;]$(echo $PREVIEW_URL)" - name: Add to Firebase Auth domain if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }} shell: bash run: | # 現状の設定を取得し、jqを活用して PREVIEW_URL を追加する DATA=$(curl --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \ --header "Accept: application/json" --silent --compressed \ "https://identitytoolkit.googleapis.com/v2/projects/${{ env.PROJECT }}/config" | \ jq --arg url ${{ steps.construct_url.outputs.url }} '.authorizedDomains + [$url] | {"authorizedDomains": .}') # authorizedDomainsをPATCHで更新 curl --request PATCH --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data "$DATA" --silent --compressed \ --output /dev/null -w %{http_code} \ "https://identitytoolkit.googleapis.com/v2/projects/${{ env.PROJECT }}/config?updateMask=authorizedDomains" - name: Delete from Firebase Auth domain if: ${{ github.event.action == 'closed' }} shell: bash run: | # 現状の設定を取得し、jqを活用して PREVIEW_URL を配列から削除する DATA=$(curl --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \ --header "Accept: application/json" --silent --compressed \ "https://identitytoolkit.googleapis.com/v2/projects/${{ env.PROJECT }}/config" | \ jq --arg url ${{ steps.construct_url.outputs.url }} '.authorizedDomains - [$url] | {"authorizedDomains": .}') # authorizedDomainsをPATCHで更新 curl --request PATCH --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --data "$DATA" --silent --compressed \ --output /dev/null -w %{http_code} \ "https://identitytoolkit.googleapis.com/v2/projects/${{ env.PROJECT }}/config?updateMask=authorizedDomains"
*1:今はAPIでも対応できるのでその旨を自分もコメントを追記しておきました。
*2:google app engine - Workaround for lack of wildcard auth options in Firebase? - Stack Overflow
*3:google app engine - Workaround for lack of wildcard auth options in Firebase? - Stack Overflow
*4:Any API to add an authorized domain to Firebase Auth? - Stack Overflow
*5:Any API to add an authorized domain to Firebase Auth? - Stack Overflow
*6:gcloud - How to configure google identity platform with CLI sdk? - Stack Overflow