electronでfirebase-authを使うときにlocalhostを承認済みドメインに入れたくない！

*この資料はナレッジワークの社内勉強会で利用したものを公開したものです

---

# 問題点tl;dr

electronのrendererプロセスでfirebase-sdkを利用する場合、URLにlocalhostを使うことになるので、firebase-authの承認済みドメインにlocalhostを許可せざるをえないという問題があります

---

# 解決策tl;dr

以下のように解決します

- mainプロセスへfirebase sdkを利用する処理を逃がす
- sign inはサーバーを用意し、firebae-adminで `createCustomToken` を使って行い、そのtokenをmainプロセスで利用する
- ただし、mainプロセス（=nodeのプロセス)上ではfirebase-authのユーザーのPersistができず、アプリ立ち上げ時に過去に取得した認証情報を取ることができない問題がある。その対処としてelectron用に独自のpersistenceを実装することで問題を解決する（参照実装は[こちら](https://github.com/sisisin-sandbox/electron-with-firebase/blob/acea70ccc06bf91ba52d5f7c189df5b620b624a3/src/main/firebase/firebase.ts#L18)）

---

# モチベーション

firebase-authは信頼できるドメイン上でのみ認証フローを通せるような設定として、 `承認済みドメイン(Authorized domain)` という設定があります
一般に本番環境で運用するアプリではこの承認済みドメインにlocalhostを追加しない（ *要出典[^1][^2] ）のですが、素直にelectronでfirebase-authを使おうとすると、localhostを許可することになってしまいます

[^1]:[Firebase セキュリティ チェックリスト](https://firebase.google.com/support/guides/security-checklist?hl=ja) などを見てもlocalhostを指定してはいけないという明示的な記載がないので、もしかしたらマジでlocalhost入れても良いのかもしれない。もしそうだったら全て徒労ですね
[^2]:とはいえ、localhostを許可しているとkeyをダウンロードして好きに利用できてしまうので、AppCheckなどを通さずにsdkを利用してサービスに対して攻撃する余地が生まれる、といった問題はありそうです
---

electronアプリはmainプロセス・rendererプロセスという2つのプロセスを扱ってアプリケーションを構築します
mainプロセスはNode.jsで動き、ネイティブAPIを利用出来ます
rendererプロセスはChromiumでwebコンテンツ（htmlなど）をレンダリングします

素直にelectronでfirebase sdkを利用する場合、rendererプロセスでlocalhostのコンテンツを表示して、その中で利用する形になるのですが、これはつまり、firebase-authの `signInWithPopup` などの関数を利用してsign inするために承認済みドメインにlocalhostを追加する必要が出てしまう、ということになるというわけです

---

ちなみにfirebaseは基本的にwebやネイティブアプリでの利用をサポートしていますが、一応デスクトップアプリ、例えば[Cordvaがサポートされていたり](https://firebase.google.com/docs/auth/web/cordova?hl=ja)するのですが、electronはサポート外なのですね

---

# 解決方法（再掲）

以下のように解決します

- mainプロセスへfirebase sdkを利用する処理を逃がす
- sign inはサーバーを用意し、firebae-adminで `createCustomToken` を使って行い、そのtokenをmainプロセスで利用する
- ただし、mainプロセス（=nodeのプロセス)上ではfirebase-authのユーザーのPersistができず、アプリ立ち上げ時に過去に取得した認証情報を取ることができない問題がある。その対処としてelectron用に独自のpersistenceを実装することで問題を解決する

---

# mainプロセスへfirebase sdkを利用する処理を逃がす

基本的にfirebase sdkはNodeでもブラウザでも動くように作られています
ただし、firebase-authのsignIn系処理はブラウザで実行することを前提としているので、node上でsignIn系の関数が実行できません
そのため、sign in処理を行うサーバーを用意し、そちらで認証を通してelectron appに戻ってくる、というフローを取ります


---

こんなシーケンスになります  

```plantuml
"electron renderer"->"electron main": signIn要求
"electron main"->"認証server" : signIn
"認証server"->"firebase auth": createCustomToken
"firebase auth"->"認証server" : token
"認証server"->"electron main":token
"electron main"->"electron main":signInWithCustomToken
"electron main"->"electron renderer": signIn succeeded
"electron renderer"->"electron main": (something request)
"electron main"->"firebase backend": (something request)
```


---

（参考: rendererプロセスで使う場合はこんな感じになります）

```plantuml
"electron renderer"->"firebase auth" : signIn
"firebase auth"->"electron renderer" : token
"electron renderer"<->"firebase backend": (something request)
```



sign inのシーケンスをこのように変更することで、localhostを承認済みドメインに含めずに済みました
実際にfirebase sdkを利用してAPIを叩くときはmainプロセスを経由して実行することになります（面倒ではありますが・・・）

---

これで全部解決とはいきません
なぜなら、firebase-authはnode上で認証情報を永続化する仕組みになっていないためです（nodeでsignIn系関数使えないんだからそりゃそう）

[認証状態の永続性](https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja)のドキュメントで記載されている 
- `firebase.auth.Auth.Persistence` の `LOCAL` , `SESSION` , `NONE` はそれぞれブラウザで言えば `localStorage` , `sessionStorage` , `オンメモリ` で保存する実装になっています

例えばこの辺が `PersistenceType.LOCAL` の実態
https://github.com/firebase/firebase-js-sdk/blob/firebase%4010.7.1/packages/auth/src/platform_browser/persistence/local_storage.ts#L54

この永続化の仕組みをどうにかする必要があります

---

さてここで[Auth の依存関係のカスタマイズ](https://firebase.google.com/docs/auth/web/custom-dependencies?hl=ja)というドキュメントを見ると、firebase-authのpersistenceは、sdkの初期化時にDI出来ることがわかります

```ts
// https://firebase.google.com/docs/auth/web/custom-dependencies?hl=ja より引用
import {initializeAuth, browserLocalPersistence} from "firebase/auth";
import {initializeApp} from "firebase/app";

const app = initializeApp({/** Your app config */});
const auth = initializeAuth(app, {
  persistence: browserLocalPersistence,
  // No popupRedirectResolver defined
});
```

---

ここは本来、firebase-auth側で実装済みのpersistenceクラスを渡す想定で、Persistenceのinterfaceも以下のように、 `readonly type: 'SESSION' | 'LOCAL' | 'NONE';` のみが定義されたものになっています


```ts
// https://github.com/firebase/firebase-js-sdk/blob/537d39982f90aff50519b8c1ad6d58048fb8f244/packages/auth/src/model/public_types.ts#L334-L342 より
export interface Persistence {
  /**
   * Type of Persistence.
   * - 'SESSION' is used for temporary persistence such as `sessionStorage`.
   * - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`.
   * - 'NONE' is used for in-memory, or no persistence.
   */
  readonly type: 'SESSION' | 'LOCAL' | 'NONE';
}
```

---

が、渡している値はJavaScriptのクラスによる実装本体なので、ここに自前のpersistenceを渡してあげれば動かすことが出来ます（！）

---

ということで、electron向けに再実装してあげて↓のように初期化してあげれば無事永続化が出来るようになります

```ts
// https://github.com/sisisin-sandbox/electron-with-firebase/blob/acea70ccc06bf91ba52d5f7c189df5b620b624a3/src/main/firebase/firebase.ts
import { initializeApp } from 'firebase/app';
import { initializeAuth } from 'firebase/auth';
import { electronLocalPersistence } from './_internal/ElectronLocalPersistence';

// ...

export const fbAuth = initializeAuth(fbApp, {
  persistence: electronLocalPersistence,
});
```

---

以上の手順で、electronでfirebase-authを利用しつつ、承認済みドメインの設定にlocalhostを入れないで済ませることが出来ました
もし参考になればこれ幸い

---