GMO Flatt Security株式会社の公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

こんにちは。 セキュリティエンジニアの@okazu-dm です。
この記事は、Firebase Authenticationに2022年7月ごろに追加されたblocking functions という機能についての紹介です。
詳細は後述しますが、blocking functionsはFirebase Authenticationの登録、サインイン処理を拡張するための機能で、この記事では、blocking functionsの概要やリリースされた経緯を紹介し、実際のユースケースも一部サンプルコードと共に例示します。
また、Flatt SecurityではFirebaseで構築されたサーバーレスなアプリケーションへの効果的な脆弱性診断メニューを提供しています。ご興味のある方はぜひ事例インタビューをご覧ください。
blocking functionsは現在Googleが提供しているIDaaSの一つであるFirebase Authenticationの一機能であり、2022年7月頃に発表1されました。
Firebase AuthenticationはIDaaSとして認証に関わる機能を提供していますが、開発者がデプロイしたCloud Functionsの関数をblocking functionsとして登録し、サインイン処理の途中で呼び出されるようにすることで、認証処理を拡張することができます。
大まかには以下の2つのことができます。
これにより例えばサービスを利用可能なユーザのメールアドレスのドメインを特定ドメインのみに制限するなど、従来はサーバサイドのアプリケーションを別に用意したり、Firestoreのセキュリティルールなど別の箇所で実施していたような柔軟な処理を実現することができます。
なお、blocking functions自体はFirebase Authenticationとは別にGoogleから提供されているIDaaSであるIdentity Platformの機能として提供されていたものであり、Firebase Authentication内でblocking functionsを利用する際も、Identity Platformを使用するようにアップグレードする必要があります。2
また、参考までにIdentity Platform側の情報についても触れておくと、以下から見れるようにIdentity Platformのblocking functionsは2020年にGAとなっています。
従来のFirebase Authenticationを使う場合、アカウントのバリデーションを細かく制御することはできなかったため、サインアップ後にサービスを利用するタイミングでサーバサイドアプリケーションのロジックやFirestoreのセキュリティルールなどでアクセスを弾く必要がありました。
例として、過去にFirebase Authenticationの注意点について紹介したこちらの記事の「落とし穴7」の対策のあたりなどが挙げられます。
しかし、blocking functionsの登場により、認証に関するバリデーションを認証のタイミングで実行することができるようになるため、セキュリティ対策を一箇所に集約しやすくなることや、ソフトウェアの構造も見通しが良くなることなどが期待できます。
注意点については、基本的にドキュメントに書いてあること(7秒の実行時間の制約や、一部の認証方法は関数をトリガーしないなど)に留意しておけば十分ですが、外部IdPと連携してアクセストークンをBlocking Functions中で利用する場合については2点注意があります。

基本的にはドキュメントに書いてある通りに使えば問題はありませんが、参考情報として検証の際の手順を記載します。
Firebaseのプロジェクトを準備し、Blazeプランに設定3 するまでの準備を完了したあと、以下の手順を実行しました。
firebase init でfunctionsのディレクトリを準備const functions= require('firebase-functions');exports.beforeCreate= functions.auth.user().beforeCreate((user, context)=>{//TODO});exports.beforeSignIn= functions.auth.user().beforeSignIn((user, context)=>{//TODO});
firebase deploy でデプロイする以下では、一部実際のコードも踏まえてユースケースについて検討していきます。
ちなみに検証した際は、firebase init のタイミングでTypeScriptを選択し、クライアントサイドはTypeScriptを利用するためwebpackを使用し、手元でwebpack-dev-serverが配信するhtmlにアクセスする形で確認しました。
Cloud Functions設定情報は以下の画像の通りです

こちらは公式ドキュメントのサンプルにもありますが、開発時に社内のメンバー限定で利用するときなど、適用可能な場面が多いため改めてこちらの記事内でも紹介します。
以下のコードをデプロイした後に設定画面上で登録します(処理内容は公式のサンプルと一緒ですがTypeScriptなので微妙に表記が違います)。
import *as functionsfrom'firebase-functions';import{AuthUserRecord, AuthEventContext}from'firebase-functions/lib/common/providers/identity';const emailDomainCheck=(user: AuthUserRecord, context: AuthEventContext):void=>{if(!user.email||!user.email.endsWith('@example.com')){thrownew functions.https.HttpsError('invalid-argument',`Unauthorized email "${user.email}"`);}}exportconst beforeCreate= functions.auth.user().beforeCreate(emailDomainCheck);exportconst beforeSignIn= functions.auth.user().beforeSignIn(emailDomainCheck);
非常にシンプルなコードですが、これで設定したドメイン以外での登録を禁止できます。実際にメールアドレス/PWによる登録などで試すと、以下のようにエラーレスポンスが帰ってくることが確認できます。

こちらは、外部のIdPを利用する際のサンプルとなることを想定して検証しました。外部のIdPから受け取ったアクセストークンを利用してユーザの情報を取得し、それに基づいたアクセス制御を行います。
このコードを検証する際には、準備が4点必要です。
ここの設定は以下の画像のようにGitHubアプリのclient id/client secretが必要なので、一旦この状態でGitHubの設定を進めます(コールバックURLはGitHubアプリに登録する必要があるので記録しておいてください

手順については公式ドキュメント通りで、コールバックURLに関しては上の手順でFirebaseの管理画面に表示されたものを入力してください。
また、ここで発行されたclient id/client secretをそれぞれFirebaseの管理画面から入力してください。
GitHubの設定画面からpersonal access tokenを発行するなどして以下のようなコマンドで自分が所属するorganizationの情報を取得することができます。
curl -v https://api.github.com/user/orgs \ -H "Accept: application/vnd.github+json" \ -H 'Authorization: Bearer ******
取得した情報の中のIDをCloud Functionsの関数のコード中で使います。
以下がサンプルコードです。
Cloud Functions側コード
import axiosfrom"axios";import *as functionsfrom'firebase-functions';import{AuthUserRecord, AuthEventContext}from'firebase-functions/lib/common/providers/identity';const githubOrgCheck=async(user: AuthUserRecord, context: AuthEventContext):Promise<void>=>{// 検証時にcloud functionsのログ画面から確認するためのログ出力console.log(`-- user --`);console.log(user);console.log(`-- context --`);console.log(context);// 外部プロバイダを使うとuser.emailはundefinedとなるconst email= user.providerData[0].emailif(!email||!email.endsWith('@gmail.com')){thrownew functions.https.HttpsError('invalid-argument',`Unauthorized email "${email}"`);}const accessToken= context.credential!.accessTokenconst orgResponse=await axios.get("https://api.github.com/user/orgs",{ headers:{ Accept:"application/vnd.github+json", Authorization:`Bearer${accessToken}`,},})const orgs:Array<any>=await orgResponse.datalet belongsToOrg=falsefor(const orgof orgs){if(org.id== ******){// 上で取得したorganization IDを入れる belongsToOrg=truebreak}}if(!belongsToOrg){thrownew functions.https.HttpsError('permission-denied',`not found valid organization`);}};exportconst beforeCreate= functions.auth.user().beforeCreate(githubOrgCheck);exportconst beforeSignIn= functions.auth.user().beforeSignIn(githubOrgCheck);
また、クライアント側のコードで以下のように、scopeを設定する必要があります。
const provider=new GithubAuthProvider()// ユーザのorganizationに関する情報を読む場合以下の2つのどちらかは必須provider.addScope('read:org');provider.addScope('user');// その他、必要に応じて以下の情報も取得可能provider.addScope('offline_access');// refresh tokenが必要な場合provider.addScope('id_token');// id tokenが必要な場合
これで試しにテストユーザ、テストorganizationを作って試してみるとorganizationに入っているユーザ以外の登録, サインインを禁止できるかと思います。
公式ドキュメントには単純な可否の判断だけでなく、ユーザのアイコン画像URLにデフォルトのものを設定するなど、ユーザ情報の設定のサンプルなどもあります。
また、FirestoreなどAuthentication以外のサービスと組み合わせて、近い時間帯の新規作成時のIPアドレスやアカウント情報などを記録し、ユーザ作成の自動化のようなサービスとして好ましくないアクセスを検知するなど、より動的でサービス仕様に寄り添ったチェックも考えられます。
以上のように、Firebase Authenticationに新しく追加されたblocking functionsについて実際の使用例も含めて紹介しました。
従来のFirebase Authenticationでは直接手を入れることができなかった登録、サインインの処理をCloud Functionsを間に挟む形で柔軟に拡張でき、まだ実用的とは言い難い部分もありますが今後改善されていくと期待して採用を検討する余地は大いにあると思われます。
さて、Flatt Security では、一般的な Web アプリケーションだけでなく、本稿で紹介した Firebase のような mBaaS を含む幅広いセキュリティ診断サービスを提供しています。また、事後的な診断に限らず開発段階でのコンサルティングも対応可能です。

上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。
また、Flatt Security はセキュリティに関する様々な発信を行っています。最新情報を見逃さないよう、公式 Twitter のフォローをぜひお願いします!
では、ここまでお読みいただきありがとうございました。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。