Firebase Admin SDKで一般的なWebサービスの構成にFirebase Authenticationを使った認証処理を組み込む。

# 概要
[Firebase #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/firebase_2)の14日目の記事です。

[Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)を使用した記事は数多くあるのですが、`一般的なWebサービスのサーバーサイドでの認証にFirebase Authenticationを利用する`記事が見当たらなかったので、サンプル実装を書いてみました。

## 今回のFirebase Authenticationの想定ユースケース

- 自前でのユーザー管理は行いたい
- 認証部分のみFirebase Authenticationを導入し、Googleアカウントとの紐付けを行いたい。認証処理の実装の省略したい。

## システム構成図と認証フロー
今回作成するFirebase Authenticationを組み込んだWebアプリのシステム構成を図にしました。

![認証-Page-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294714/fa6c4add-bff5-ccfc-c484-c51386bf413d.jpeg)

以下のフローでFirebase AuthenticationによるセキュアなAPI実装を実現します。

- ① なんらかの認証トリガー（ログインボタンをクリックなど）から特定の認証プロバイダ（今回はGoogle）の認証画面へリダイレクトする。
- ②③ Firebase Authenticationが認証処理を行い、IDトークンが払い出される。
- ④ 何らかの認証が必要な処理を行うHTTPリクエストを送る。
    - その際、`Authorization`ヘッダーに取得したIDトークンを付与してリクエストする。
- ⑤ Firebase Admin SDKでIDトークンの検証を行う
- ⑥ するとユーザーIDやEmailアドレスなどが取得できるので、それを使って自前のUser Tableと突合し、正規のユーザーか判断・ユーザー情報の取得等ができるようになる。

## サンプルアプリの仕様

今回作成するサンプルアプリの仕様です。

- ログインはGoogleの認証画面へのリダイレクトによって行う。
- ユーザーがその画面に来た時、そのユーザーがログイン済みであればそのユーザーの情報を画面に表示する。
    - その情報は他の一般ユーザーが閲覧することはできない。
- ユーザーがログイン済みでなければ、ログイン画面（`/login`）に飛ばす。

## サンプルアプリでの採用言語＋ライブラリ等

- クライアントサイド : TypeScript + Axios
- サーバーサイド : Node.js + TypeScript + Express

# クライアントサイドでFirebase Authenticationによるログイン処理とAPIへのリクエストを行う

まずはクライアントサイドの実装からしていきましょう。

## 事前準備

### Firebaseのプロジェクトを作成
先の方々に説明を譲ります。

### SDKの取得
npmでFirebase SDKを取得します。

```bash
npm i firebase
```

### 設定JSONの取得
Firebaseの設定をJSONで取得し、アプリケーションとFirebaseの紐付けを行います。
公式の記事を参考にしましょう。
https://firebase.google.com/docs/web/setup?hl=ja

*Firebase Hostingを使ってHTMLのホスティングを行う場合はこの設定JSONは必要ありませんが、ローカルでの確認する際など、あると便利なので一応取得することをオススメします。*

## 実装

### Firebase SDKを初期化

`firebase.initializeApp`に先ほど取得した設定JSONを渡します。

```ts
import firebase from "firebase/app";
import "firebase/auth";

const firebaseConfig = {
  /* 先ほど取得した設定JSONオブジェクト */
}
firebase.initializeApp(firebaseConfig);
```

### ログイン処理を実装

リダイレクトによるログインを実装したい場合は以下
プロバイダをGoogleに指定する例です。

```ts
const login = () => {
  const provider = new firebase.auth.GoogleAuthProvider();
  firebase.auth().signInWithRedirect(provider);
};
```

ログイン自体の処理はこれで終わりです。あっけないですね。
これだけで実際にGoogleの認証画面へリダイレクトして、戻ってくる動きが確認できます。

### 認証情報の取得

ログインした。という情報およびIDトークンを受け取ります。
認証情報はJSのロードから少し時間をおいて取得されるので、認証情報を使用する場合はそれが取得されたことをSubcribeする必要があります。
Subcribeには`onAuthStateChanged`メソッドを使用します。

```ts
// firebase.auth().getRedirectResultはあまり使い勝手が良くなかった。
firebase.auth().onAuthStateChanged(async currentUser => {
    if (currentUser) {
      // ここでログインユーザーの情報が参照できる。
      const { email, uid, displayName } = currentUser;
      console.log(email, displayName, uid);
      // IDトークンを取得する。
      const idToken = await currentUser.getIdToken();
      console.log(idToken);
    } else {
      /* 
        未ログイン時にはcurrentUserがnullで渡ってくるので
        nullチェックでfalseな分岐に未ログイン時の処理を記述する。
      */
      window.location.href = "/login";
    }
  });
```


`getRedirectResult`メソッドでもリダイレクト後の認証情報の取得は可能なのですが、それ以外のケース（すでにログイン済みのユーザーがページに訪れた時）などでの使い勝手が良くなかったで採用しませんでした。


### 認証情報を使ってAPIにアクセスする。

サーバーサイドでのアクセスの検証に用いるため、IDトークンをHTTPヘッダーにつけてリクエストを飛ばします。

```ts
firebase.auth().onAuthStateChanged(async currentUser => {
    if (currentUser) {
      const idToken = await currentUser.getIdToken();
      // 何らかの認証が必要なリクエストをIDトークン付きで飛ばす
      const res = await axios.get(
        BACKEND_SERVICE_BASE_URL + "/secret/userinfo",
        {
          headers: {
            Authorization: idToken
          }
        }
      );
      console.log(res.data.user.secretData);
    } else {
      window.location.href = "/login";
    }
  });
```

クライアントサイドは以上です。

# Firebase Admin SDKを使ってサーバーサイドでIDトークンを検証する。

さて、次はこのアクセスをサーバーサイドで検証するコードを実装してみましょう。

## 事前準備

### SDKの取得

npmでFirebase Admin SDKを取得します。

```bash
npm i firebase-admin
```

### 秘密鍵の生成

1. Firebaseのコンソールから「プロジェクトの設定」 -> 「サービスアカウント」 を選択
2. ページ下部にある「新しい秘密鍵の生成」を押下します。
3. JSONがダウンロードできるのでそれを任意の場所に保存します。

きちんとした手順は公式を参照しましょう。（丸投げ）
https://firebase.google.com/docs/admin/setup?hl=ja

## 実装

### 秘密鍵の適用とSDKの初期化

環境変数`FIREBASE_CONFIG`に**先ほど保存したJSONのファイルパス**、`GOOGLE_CLOUD_PROJECT`にプロジェクト名を指定し、サーバーを起動させます。

```bash
FIREBASE_CONFIG='path/to/secret.json' GOOGLE_CLOUD_PROJECT='projectname' node server.js #server.jsはコンパイル後のjsファイル
```

SDKを初期化させる際はSDKが上記で設定した環境変数を勝手に見に行くので引数を渡す必要はありません。

```ts
import Admin from "firebase-admin";

const admin = Admin.initializeApp();
```

### APIでのIDトークンの検証

ExpressでAPIの処理を実装します。
`verifyIdToken`メソッドを使い、クライアントから送られてきた`Authorization`ヘッダーのIDトークンを検証し`uid`を取得します。`uid`はユーザーごとに一意の値になるので、これをキーに別テーブルにてユーザ管理を行えば、Firebase Authenticationと自前のユーザー管理を紐づけることが可能です。

```ts
const app = express();

// 諸々のミドルウェアの適用は省略

app.get("/secret/userinfo", async (req, res) => {
  const idToken = req.header("Authorization");
  if (idToken) {
    const {uid} = await admin.auth().verifyIdToken(idToken);
    // uid を使って紐付けられたユーザー情報を取得する処理を行ったりする。
    const someUseInfo = userService.getInfo(uid);
    res.json(someUseInfo);
  }
  // Authorizationヘッダーが無ければ403
  res.status(403).send();
});

```

以上です。

# まとめ

簡単とは行かないまでも、小難しい認証処理をFirebaseに移譲できたのが良かったです。
今回は時間がなく準備できなかったのですが、後々サンプルコードを公開する予定ですので良くわからなかった方は参照ください。