HTTPv1による認証とFirebaseでiOSアプリのプッシュ通知を実装した話

# HTTPv1による認証とFirebaseでiOSアプリのプッシュ通知を実装した話
## はじめに

iOSアプリでFirebase Cloud Messaging（FCM）を利用したプッシュ通知を実装する際に、**HTTPv1 APIを使った認証**と**Firebase Cloud Functions**を導入した。本記事では、なぜこの2つを採用したのか、具体的な設定方法と実装のポイントを説明する。

## HTTPv1 APIとCloud Functionsの採用理由

### HTTPv1 APIとは？

Firebase Cloud Messaging（FCM）では、クライアントアプリ（iOSやAndroid）にプッシュ通知を送信するためのAPIが提供されている。このAPIには、かつて**レガシーAPI（Legacy HTTP API）**が使用されていたが、**2024年7月22日をもって廃止**され、現在は**HTTPv1 APIが唯一の正式な方法**となっている【公式ドキュメント参照】([Firebase公式ドキュメント](https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ja))。

#### HTTPv1 APIの特長
- **IAMによる認証管理**  
  - Google Cloud IAM（Identity and Access Management）を活用し、より細かいアクセス制御が可能。
- **通知の柔軟なカスタマイズ**  
  - JSONペイロードの構造が拡張され、通知の内容や送信方法を細かく設定できる。
- **複数の受信者への一括送信**  
  - トピックベースやデバイスグループを利用し、一括通知が可能。

### Firebase Cloud Functionsとは？

**Firebase Cloud Functions**は、Google Cloudのサーバーレス環境で実行されるコードをホスティングするサービスである。  
Firebase上でイベントが発生した際に、そのイベントに応じた処理を自動的に実行することができる。

#### Cloud Functionsの特長
- **サーバー管理不要のサーバーレス環境**  
  - 自前でサーバーを用意せずに、イベントに応じた処理を記述・実行できる。
- **Firestoreや認証との連携が容易**  
  - Firestoreのデータ変更、ユーザーの認証状態変更などをトリガーにして、処理を自動化できる。
- **HTTPリクエストのエンドポイントを提供できる**  
  - **iOSアプリなどのクライアントが直接呼び出せるAPIを作成可能**で、セキュアな通知送信を実装できる。

公式ドキュメントはこちら：
[Firebase Cloud Functions](https://firebase.google.com/docs/functions?hl=ja)

### なぜCloud Functionsを採用したのか？

HTTPv1 APIを利用するには、**クライアントアプリ（iOS）から直接Firebaseにリクエストを送るのではなく、サーバー経由で認証を行うのが推奨されている**。そのため、**Firebase Cloud Functions**を活用し、バックエンドで通知を処理する方式を採用した。

#### Cloud Functionsを利用するメリット
1. **FCMの認証情報をクライアントアプリに持たせずに済む**  
   - クライアントアプリから直接HTTPv1 APIを呼び出すと、Googleの認証トークンをアプリ内で管理する必要がある。しかし、これは**セキュリティリスク**となる。
   - **Cloud Functionsを経由することで、サーバーサイドで認証情報を管理できる**。
2. **スケーラブルな通知処理が可能**  
   - 複数のユーザーへの一括通知、特定のグループへの通知送信を簡単に処理できる。
3. **Firestoreと連携しやすい**  
   - Firestoreに保存されている**ユーザーのデバイス情報やトークンを基に、自動で通知を送信**できる。

## ディレクトリ構造と関係ファイル

プッシュ通知に関連するコードは、以下のようなディレクトリ構造で管理した。

```
📂 SharedShoppingList
 ├── 📂 Firebase
 │    ├── NotificationManager.swift  // 通知の設定と送信を担当
 │    ├── SessionManager.swift       // ユーザーのFCMトークンを管理
 │    ├── AppDelegate.swift          // Firebaseの初期化とAPNs登録
 ├── 📂 CloudFunctions
 │    ├── index.js                   // Firebase Cloud Functionsで通知を送信
 ├── 📂 Views
 │    ├── SettingsView.swift         // 通知設定のUI
```

## Firebase Cloud Messagingの設定手順

### Firebaseプロジェクトの作成とCloud Messagingの設定

1. **Firebase Console** にアクセスし、新規プロジェクトを作成する。
2. **Cloud Messaging** を有効化する。
   - 「プロジェクト設定」>「Cloud Messaging」に移動し、「APNs認証キー」を追加。

### APNsの設定とFCMの連携

iOSアプリでプッシュ通知を受信するために、**Apple Push Notification Service（APNs）**とFirebaseを統合する。

#### `AppDelegate.swift`
```swift
import Firebase
import FirebaseMessaging
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate, MessagingDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        FirebaseApp.configure()
        NotificationManager.shared.configure() // 🔥 通知設定
        Messaging.messaging().delegate = self
        return true
    }

    // 🔥 APNsトークンの取得
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }
}
```
**このコードを `AppDelegate.swift` に記述する**。

## Firebase Cloud Functionsで通知を送信する

### Google Cloudの設定（IAMと認証）

1. Firebase Consoleの「プロジェクト設定」→「サービスアカウント」を開く。
2. 「新しい秘密鍵を生成」を選択し、JSONファイルをダウンロード。
3. Google Cloud IAMで、「Firebase Cloud Messaging API」を有効化する。

### Cloud Functionsの実装

#### `index.js`
```javascript
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { google } = require("googleapis");

admin.initializeApp();

const PROJECT_ID = "プロジェクトID";
const SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"];

async function getAccessToken() {
  const auth = new google.auth.GoogleAuth({ scopes: SCOPES });
  const client = await auth.getClient();
  const tokens = await client.getAccessToken();
  return tokens.token;
}

exports.sendPushNotification = functions.https.onRequest(async (req, res) => {
  const { token, title, body } = req.body;
  if (!token || !title || !body) return res.status(400).send("Missing fields");

  try {
    const accessToken = await getAccessToken();
    const message = {
      message: {
        token: token,
        notification: { title, body },
      },
    };

    const response = await fetch(`https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(message),
    });

    res.status(200).send(await response.json());
  } catch (error) {
    res.status(500).send("Error sending notification");
  }
});
```
**このコードを `CloudFunctions/index.js` に記述する**。

## 参考文献

- [Firebase Cloud Messaging - HTTPv1 APIへの移行](https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ja)
- [Firebase Cloud Functions 公式ドキュメント](https://firebase.google.com/docs/functions?hl=ja)