Node.js + FirebaseでStatelessなAPIを実装する

## 留意事項

メモ書き程度なので詳細な説明は割愛します。

## 使う技術やサービス

* Firebase Authentication
* Node.js（FWはExpress）

## Firebase側の準備

プロジェクト作成し、Authentication -> Sign-in MethodでGoogleを有効にしておきます。
 Firebase側の準備はこれで終わりです。

## 実装

firebase-toolsをグローバルではなくローカルにインストールして進めていきます（ちょっと訳あり）。

### 準備

適当なディレクトリを作って

```sh
$ npm init -y
$ npm i firebase-tools
$ npx firebase login
```

ログインが終わったら

```sh
$ npx firebase init
```

以下、ログ

```sh
?  Are you ready to proceed? (y/N)  y

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
 ( ) Database: Deploy Firebase Realtime Database Rules
 ( ) Firestore: Deploy rules and create indexes for Firestore
 (*) Functions: Configure and deploy Cloud Functions
 (*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features

? Please select an option:
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project

? Select a default Firebase project for this directory: (Use arrow keys)
> hogehoge-xxxxx (hogehoge)

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? (Use arrow keys)
> JavaScript
  TypeScript

? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) n
+  Wrote functions/package.json
+  Wrote functions/index.js
+  Wrote functions/.gitignore

? Do you want to install dependencies with npm now? (Y/n) y

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

+  Firebase initialization complete!
```

### バックエンド

functionsディレクトリ内で作業する。

```sh
$ npm i express cookie-parser
```

index.js

```js
const admin = require("firebase-admin");
const cookieParser = require("cookie-parser")();
const express = require("express");
const functions = require("firebase-functions");

const api = express();

admin.initializeApp();

const validateFirebaseIdToken = async (req, res, next) => {
  if (
    (!req.headers.authorization ||
      !req.headers.authorization.startsWith("Bearer ")) &&
    !(req.cookies && req.cookies.__session)
  ) {
    res.status(403).send("Unauthorized");
    return;
  }

  let idToken;
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith("Bearer ")
  ) {
    console.log('Found "Authorization" header');
    idToken = req.headers.authorization.split("Bearer ")[1];
  } else if (req.cookies) {
    console.log('Found "__session" cookie');
    idToken = req.cookies.__session;
  } else {
    res.status(403).send("Unauthorized");
    return;
  }

  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken);
    console.log("ID Token correctly decoded", decodedIdToken);
    req.user = decodedIdToken;
    next();
    return;
  } catch (err) {
    console.error(err);
    res.status(403).send("Unauthorized");
    return;
  }
};

api.use(cookieParser);
api.use(validateFirebaseIdToken);

api.get("/", (req, res) => {
  res.status(200).send(JSON.stringify(req.user));
});

exports.api = functions.https.onRequest(api);
```

### フロントエンド

publicディレクトリ内で作業する。

index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Sample</title>
  </head>
  <body>
    <button id="demo-sign-in-button" style="display: none;">
      Sign in with Google
    </button>
    <button id="demo-sign-out-button" style="display: none;">
      Sign out
    </button>

    <script src="/__/firebase/7.14.2/firebase-app.js"></script>
    <script src="/__/firebase/7.14.2/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script src="index.js"></script>
  </body>
</html>
```

index.js

```js
document.addEventListener("DOMContentLoaded", function () {
  signInButton = document.getElementById("demo-sign-in-button");
  signOutButton = document.getElementById("demo-sign-out-button");

  signInButton.addEventListener("click", signIn);
  signOutButton.addEventListener("click", signOut);
  firebase.auth().onAuthStateChanged(onAuthStateChanged);
});

const onAuthStateChanged = (user) => {
  if (user) {
    console.info(user);
    signInButton.style.display = "none";
    signOutButton.style.display = "block";
    startFunctionsCookieRequest();
  } else {
    console.info("!user");
    signInButton.style.display = "block";
    signOutButton.style.display = "none";
  }
};

const signIn = () => {
  firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider());
};

const signOut = () => {
  firebase.auth().signOut();
  document.cookie = "__session=";
};

const startFunctionsCookieRequest = () => {
  firebase
    .auth()
    .currentUser.getIdToken(true)
    .then(function (token) {
      document.cookie = "__session=" + token + ";max-age=86400";
    });
};
```

プロダクト開発では`Authorization: Bearer <token>`を使うようにしよう（手抜き）。


## 動作確認

プロジェクトルートディレクトリで作業する。

```sh
$ npx firebase emulators:start
```

Hostingは5000番ポート、Functionsは5001番ポートで起動しますのでそれぞれ確認。