Flatt Security Blog

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

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

Firebase Authenticationのバリデーション等を新機能「blocking functions」を用いて拡張する

こんにちは。 セキュリティエンジニアの@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つのことができます。

これにより例えばサービスを利用可能なユーザのメールアドレスのドメインを特定ドメインのみに制限するなど、従来はサーバサイドのアプリケーションを別に用意したり、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 するまでの準備を完了したあと、以下の手順を実行しました。

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 のフォローをぜひお願いします!

twitter.com

では、ここまでお読みいただきありがとうございました。


  1. https://firebase.blog/posts/2022/07/new-firebase-auth-features
  2. https://firebase.google.com/docs/auth#identity-platform
  3. Cloud Functionsには無料枠はあるものの従量課金のBlazeプランにアップグレードする必要があります https://firebase.google.com/pricing