Firebase の API キーは公開しても大丈夫?

こんにちは、クラウドエース フロントエンド・UI/UX 部の 小堀内 です。
Firebase を使用する開発者の間で、API キーの取り扱いについてよく議論されています。
「API キーは絶対に公開してはいけない」という考えが一般的ですが、Firebase の場合は少し事情が異なります。
この記事では、Firebase の API キーの特性と適切な管理方法について解説します。
結論
- Firebase の API キーは公開しても基本的に問題ない
 - ただし、Firebase の API キーは公開される前提で設計されているため Firebase プロジェクト側での対策が必要
- Firebase Security Rules の適切な設定
 - Firebase Authentication によるユーザー認証の実装
 - (オプション) Firebase App Check の導入
 
 
Firebase の API キーとは
クライアントアプリケーションが Firebase サービスにアクセスするための識別子となります。
また、下記の内容が 公式ドキュメント_Firebase の API キーの使用と管理について学ぶ にて言及されています。
Firebase の API キーは、通常の API キーとは異なる
API キーの一般的な使用方法とは異なり、Firebase サービスの API キーは、バックエンド リソースへのアクセス制御には使用されません。
バックエンド リソースへのアクセス制御は、Firebase セキュリティ ルール(リソースにアクセスできるユーザーを制御)と、App Check(リソースにアクセスできるアプリを制御)のみによって行います。通常、API キーは細心の注意を払って保護する必要があります(たとえば Vault サービスを使用したり、キーを環境変数として設定したりするなど)。
しかし、Firebase サービスの API キーは、コードまたはチェックインされた構成ファイルに含めても問題ありません。
Firebase の API キーの特徴
Firebase の API キーには、以下のような特徴があります。
| 特徴 | 説明 | 
|---|---|
| 公開を前提としている | クライアントサイドで使用するため、完全に秘匿することは困難 | 
| 制限された権限 | API キー単体では、重要なデータへのアクセスや管理操作はできない | 
| 追加のセキュリティ層 | Firebase Security Rules や認証の組み合わせと Firebase App Check を使用する | 
1. 公開を前提としている
Firebase の API キーは、クライアントサイドのコードで使用することを前提に設計されています。
そのため、クライアントサイドのアプリケーションで Firebase SDK を初期化して、Firestore, Realtime Database, Cloud Storage for Firebase, Firebase Authentication など、各種サービスにアクセスするために、このキーが必要になるということです。
クライアントサイドでの Firebase SDK 初期化例
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
    show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
///   options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      return web;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return android;
      case TargetPlatform.iOS:
        return ios;
      case TargetPlatform.macOS:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for macos - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      case TargetPlatform.windows:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for windows - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      case TargetPlatform.linux:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for linux - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      default:
        throw UnsupportedError(
          'DefaultFirebaseOptions are not supported for this platform.',
        );
    }
  }
  static const FirebaseOptions web = FirebaseOptions(
    apiKey: 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    appId: '1:123456789012:web:abcdef1234567890abcdef',
    messagingSenderId: '123456789012',
    projectId: 'your-project-id',
    authDomain: 'your-project-id.firebaseapp.com',
    storageBucket: 'your-project-id.appspot.com',
  );
  static const FirebaseOptions android = FirebaseOptions(
    apiKey: 'AIzaSyYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
    appId: '1:123456789012:android:abcdef1234567890abcdef',
    messagingSenderId: '123456789012',
    projectId: 'your-project-id',
    storageBucket: 'your-project-id.appspot.com',
  );
  static const FirebaseOptions ios = FirebaseOptions(
    apiKey: 'AIzaSyZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ',
    appId: '1:123456789012:ios:abcdef1234567890abcdef',
    messagingSenderId: '123456789012',
    projectId: 'your-project-id',
    storageBucket: 'your-project-id.appspot.com',
    iosBundleId: 'com.example.yourApp',
  );
}
クライアントサイドからの Firebase サービスへのアクセス例
/// [FirebaseAuth] のインスタンスを提供する [Provider].
final _authProvider = Provider<FirebaseAuth>((_) => FirebaseAuth.instance);
/// [FirebaseAuth] の [User] を返す [StreamProvider].
/// ユーザーの認証状態が変更される(ログイン、ログアウトする)たびに更新される。
final authUserProvider = StreamProvider<User?>(
  (ref) => ref.watch(_authProvider).userChanges(),
);
/// 現在のユーザー ID を提供する [Provider].
final userIdProvider = Provider<String?>((ref) {
  ref.watch(authUserProvider);
  return ref.watch(_authProvider).currentUser?.uid;
});
/// ユーザーがログインしているかどうかを示す bool 値を提供する Provider.
/// [userIdProvider] の変更を watch しているので、ユーザーの認証状態が変更される
/// たびに、この [Provider] も更新される。
final isSignedInProvider = Provider<bool>(
  (ref) => ref.watch(userIdProvider) != null,
);
/// [FirebaseAuthRepository] のインスタンスを提供する [Provider]
final firebaseAuthRepositoryProvider = Provider<FirebaseAuthRepository>(
  (ref) => FirebaseAuthRepository(auth: ref.watch(_authProvider)),
);
/// [FirebaseAuth] を操作する Repository
class FirebaseAuthRepository {
  FirebaseAuthRepository({
    required this.auth,
  });
  final FirebaseAuth auth;
  /// 匿名認証を実施する
  Future<String> signinAnonymously() async {
    try {
      // Firebase Auth の匿名認証を実施する
      final userCredential = await auth.signInAnonymously();
      return userCredential.user!.uid;
    } on FirebaseAuthException catch (e) {
      throw e.toAuthException();
    }
  }
}
2. 制限された権限
Firebase の API キーは、以下のような制限された権限しか持っていません。
これらの制限は、Firebase (Google Cloud) コンソールでの設定によって実現されています。
- Firebase プロジェクトの識別
 - クライアント SDK の初期化
 - 公開データへの読み取りアクセス
 
実際に、Google Cloud コンソールから、Firebase の API キーを確認してみると、使用する API が制限されていることがわかります。

また、重要なポイントとして、この API キーだけでは以下のような操作は制限されており、以降で説明する Security Rules の適切な設定と認証の組み合わせによって初めて可能になるということです。
- Firestore, Realtime Database のデータ読み取り/書き込み
 - Cloud Storage for Firebase へのファイルアップロード
 - Cloud Functions for Firebase の実行
 
つまり、API キーは Firebase プロジェクトへのアクセスを可能にする「鍵」ですが、その「鍵」が開ける「ドア」の先にある各リソースへのアクセスは、さらに細かい権限設定によって制御されるということです。
3. 追加のセキュリティ層
Firebase のセキュリティは、主に
- Firebase Security Rules (リソースにアクセスできるユーザーの制御)
 - Firebase App Check (リソースにアクセスできるアプリの制御)
 
によって強化され、API キーの制限を補完する重要な役割を果たします。
Firebase Security Rules
Firebase Security Rules を使用することで、Firestore, Realtime Database, Cloud Storage for Firebase へのアクセスを細かく制御できます。

例えば Firestore に対する Security Rules は以下のように設定することができます。
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザーが認証されているかチェックする関数
    function hasAuth() {
      return request.auth.uid != null;
    }
    // リクエストしているユーザーが指定された uid の所有者かチェックする関数
    function isOwner(uid) {
      return hasAuth() && request.auth.uid == uid;
    }
    // users コレクション中のドキュメントに対するルール
    match /users/{uid} {
      allow get: if isOwner(uid);       // 自身のドキュメント取得のみ許可
      allow list: if false;             // 複数ドキュメント取得は不可
      allow create: if false;           // 新規作成は不可(Cloud Functions からのみ作成される)
      allow update: if isOwner(uid);    // 自身のドキュメント更新のみ許可
      allow delete: if isOwner(uid);    // 自身のドキュメント削除のみ許可
      // ユーザーの設定ドキュメントに対するルール
      match /conf/{confId} {
        // 以下の操作はすべて、ドキュメント所有者のみに許可
        allow get: if isOwner(uid);
        allow list: if isOwner(uid);
        allow create: if isOwner(uid);
        allow update: if isOwner(uid);
        allow delete: if isOwner(uid);
      }
    }
  }
}
一方で、Security Rules を設定しない場合、下記の操作が可能となります。
| 操作 | 説明 | リスク | 
|---|---|---|
| データベース全体の読み取り | 認証されていないユーザーを含む誰でも、データベース内のすべてのドキュメントやコレクションを自由に閲覧できる | データの漏洩、プライバシーの侵害 | 
| データの無制限な書き込み | 誰でもデータベースに新しいドキュメントを作成したり、既存のドキュメントを更新・削除したりできる | データの改ざん、不正なデータの挿入 | 
| ストレージの無制限アクセス | Cloud Storage for Firebase 内のすべてのファイルを誰でもダウンロード、アップロード、削除できる | 機密ファイルの漏洩、不正なコンテンツのアップロード | 
| 認証なしのデータ操作 | ユーザー認証を経ずに、すべてのデータ操作が可能になる | なりすまし、不正アクセス | 
| 大量のクエリ実行 | 制限なくクエリを実行できるため、データベースに過度の負荷をかける操作が可能になる | サービスの過負荷、コストの増大 | 
そのため、適切な Security Rules の設定が Firebase プロジェクト管理において非常に重要となります。
詳しい Security Rules の設定方法に関しては、Firestore Security Rules の書き方と守るべき原則 が参考になります。
Firebase App Check
承認されたアプリからのリクエストのみを Firebase バックエンドサービスが受け入れるようにするという Firebase App Check を活用することで、さらなる Firebase プロジェクトへのセキュリティを強化することが可能となります。
例えば、GitHub リポジトリに Firebase の API キーを公開していた場合、Firebase Security Rules によって、データへの不正アクセスは防げますが、API キーを使用した不正なリクエストは可能です。
この不正リクエストを防ぎたい場合に Firebase App Check を導入するということになります。

さいごに
今回は、Firebase の API キー管理の方法をご紹介させていただきました。
Firebase はクライアントサイドから直接アクセスすることを前提に設計されているため、API キーの取り扱いが従来のバックエンドサービスとは異なります。
この特性を理解することで、より効果的なセキュリティ対策に集中できるでしょう。
私自身も、個人開発でリリースしているアプリケーションのバックエンドサービスとして Firebase を使用しているくらい、大好きなサービスなので、今後も引き続き Firebase に対する知見を深めていきます。
Discussion