こんにちは。 セキュリティエンジニアの@okazu-dm です。
この記事は、Firebase Authenticationに2022年7月ごろに追加されたblocking functions という機能についての紹介です。
詳細は後述しますが、blocking functionsはFirebase Authenticationの登録、サインイン処理を拡張するための機能で、この記事では、blocking functionsの概要やリリースされた経緯を紹介し、実際のユースケースも一部サンプルコードと共に例示します。
また、Flatt SecurityではFirebaseで構築されたサーバーレスなアプリケーションへの効果的な脆弱性診断メニューを提供しています。 ご興味のある方はぜひ事例インタビューをご覧ください。
blocking functionsについての概要
blocking functionsは現在Googleが提供しているIDaaSの一つであるFirebase Authenticationの一機能であり、2022年7月頃に発表1されました。
Firebase AuthenticationはIDaaSとして認証に関わる機能を提供していますが、開発者がデプロイしたCloud Functionsの関数をblocking functionsとして登録し、サインイン処理の途中で呼び出されるようにすることで、認証処理を拡張することができます。
大まかには以下の2つのことができます。
- 登録/サインインの可否を判断する
- 関数がエラーを返すことで登録/サインインの処理をブロックすることができます https://firebase.google.com/docs/auth/extend-with-blocking-functions#blocking_registration_or_sign-in
- ID/PWの確認など通常の処理は独立して行われるため、追加のバリデーションを用意するという形です
- 表示名など一部の変更可能なユーザ情報を上書きする
- 変更可能な情報はドキュメントを参照してください https://firebase.google.com/docs/auth/extend-with-blocking-functions#modifying_a_user
これにより例えばサービスを利用可能なユーザのメールアドレスのドメインを特定ドメインのみに制限するなど、従来はサーバサイドのアプリケーションを別に用意したり、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の管理画面上で設定をオンにしないとアクセストークンがBlocking Functionsに渡されないこと
- Blocking Functionsをデプロイすると、上でオンにしたアクセストークンの共有設定がオフになるため、デプロイのタイミングで登録/サインインの失敗が発生する可能性があること(2022/11/07 現在)
- これは恐らくサービスの意図した挙動ではないと思われますが、現状はデプロイの度に手動でチェックを付ける必要があります
- 以下は検証の際に保存した管理画面のキャプチャで、こちらのチェックボックスがデプロイのあとには全て未選択の状態に戻されます
使い方
基本的にはドキュメントに書いてある通りに使えば問題はありませんが、参考情報として検証の際の手順を記載します。
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
でデプロイする- 設定画面からbeforeCreate, beforeSignInそれぞれについて(または必要な方のみ)デプロイした関数を呼び出すように設定する
- 実際にアプリケーションから触って試してみる
ユースケース
以下では、一部実際のコードも踏まえてユースケースについて検討していきます。
ちなみに検証した際は、firebase init
のタイミングでTypeScriptを選択し、クライアントサイドはTypeScriptを利用するためwebpackを使用し、手元でwebpack-dev-serverが配信するhtmlにアクセスする形で確認しました。
Cloud Functions設定情報は以下の画像の通りです
1. メールアドレスのドメイン部分を見て特定ドメインだけ通す
こちらは公式ドキュメントのサンプルにもありますが、開発時に社内のメンバー限定で利用するときなど、適用可能な場面が多いため改めてこちらの記事内でも紹介します。
以下のコードをデプロイした後に設定画面上で登録します(処理内容は公式のサンプルと一緒ですがTypeScriptなので微妙に表記が違います)。
import * as functions from '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')) { throw new functions.https.HttpsError( 'invalid-argument', `Unauthorized email "${user.email}"`); } } export const beforeCreate = functions.auth.user().beforeCreate(emailDomainCheck); export const beforeSignIn = functions.auth.user().beforeSignIn(emailDomainCheck);
非常にシンプルなコードですが、これで設定したドメイン以外での登録を禁止できます。 実際にメールアドレス/PWによる登録などで試すと、以下のようにエラーレスポンスが帰ってくることが確認できます。
2. GitHubで特定のorganizationに所属しているユーザだけを通す
こちらは、外部のIdPを利用する際のサンプルとなることを想定して検証しました。 外部のIdPから受け取ったアクセストークンを利用してユーザの情報を取得し、それに基づいたアクセス制御を行います。
このコードを検証する際には、準備が4点必要です。
(1) Firebaseの管理画面からサインイン方法としてGitHubを追加
ここの設定は以下の画像のようにGitHubアプリのclient id/client secretが必要なので、一旦この状態でGitHubの設定を進めます(コールバックURLはGitHubアプリに登録する必要があるので記録しておいてください
(2) GitHub上でOAuthアプリを作成する
手順については公式ドキュメント通りで、コールバックURLに関しては上の手順でFirebaseの管理画面に表示されたものを入力してください。
また、ここで発行されたclient id/client secretをそれぞれFirebaseの管理画面から入力してください。
(3) 通したいorganizationのorganization IDを調べる
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の関数のコード中で使います。
(4) 関数をデプロイしたあとに管理画面のblocking functions設定画面からアクセストークンを渡すように設定する(現状、この操作はデプロイの度に毎回必要です)
以下がサンプルコードです。
Cloud Functions側コード
import axios from "axios"; import * as functions from '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].email if (!email || !email.endsWith('@gmail.com')) { throw new functions.https.HttpsError( 'invalid-argument', `Unauthorized email "${email}"`); } const accessToken = context.credential!.accessToken const 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.data let belongsToOrg = false for (const org of orgs) { if (org.id == ******) { // 上で取得したorganization IDを入れる belongsToOrg = true break } } if (!belongsToOrg) { throw new functions.https.HttpsError( 'permission-denied', `not found valid organization`); } }; export const beforeCreate = functions.auth.user().beforeCreate(githubOrgCheck ); export const 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に入っているユーザ以外の登録, サインインを禁止できるかと思います。
3. その他のアイデア
公式ドキュメントには単純な可否の判断だけでなく、ユーザのアイコン画像URLにデフォルトのものを設定するなど、ユーザ情報の設定のサンプルなどもあります。
また、FirestoreなどAuthentication以外のサービスと組み合わせて、近い時間帯の新規作成時のIPアドレスやアカウント情報などを記録し、ユーザ作成の自動化のようなサービスとして好ましくないアクセスを検知するなど、より動的でサービス仕様に寄り添ったチェックも考えられます。
まとめ
以上のように、Firebase Authenticationに新しく追加されたblocking functionsについて実際の使用例も含めて紹介しました。
従来のFirebase Authenticationでは直接手を入れることができなかった登録、サインインの処理をCloud Functionsを間に挟む形で柔軟に拡張でき、まだ実用的とは言い難い部分もありますが今後改善されていくと期待して採用を検討する余地は大いにあると思われます。
さて、Flatt Security では、一般的な Web アプリケーションだけでなく、本稿で紹介した Firebase のような mBaaS を含む幅広いセキュリティ診断サービスを提供しています。また、事後的な診断に限らず開発段階でのコンサルティングも対応可能です。
上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。
また、Flatt Security はセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式 Twitter のフォローをぜひお願いします!
では、ここまでお読みいただきありがとうございました。
- https://firebase.blog/posts/2022/07/new-firebase-auth-features↩
- https://firebase.google.com/docs/auth#identity-platform↩
- Cloud Functionsには無料枠はあるものの従量課金のBlazeプランにアップグレードする必要があります https://firebase.google.com/pricing↩