Firebase HostingでSPA(React)表示 + Basic認証を同時に実現する


Firebase HostingにはBasic認証かけたりIP制限等の機能が標準では提供されていません。
それを実現するためにはFunctionsの機能と連携して実現します。

## 方針

* まず、リクエストをpublicではなくfunctionsの(app)関数にリクエストを仕向ける。
* rewrite先の関数で何を表示するかを決める。
* 表示するまでに認証等の処理をかますことにより必要な処理を行う。

## 作業場所の準備

作業場所を作ってfirebase initをします。

```bash
cd
mkdir rewrite
cd rewrite
firebase init
```

FunctionsとHostingを選択。
下記のようなフォルダが作成されるはずです。

```bash
.
├── firebase.json
├── functions
└── public
```

まず、public内のindex.htmlを削除しておきます。ファイルが存在するとそちらが優先して表示されてしまうようです。

次にfunctionsでcreate-react-appを実行します。
いろいろ編集したらbuildして、buildフォルダを作成しておきます。

```bash
cd functions
create-react-app react-app
yarn build
```

公開サイト/ファイルはpublicではなく、functions/react-app/build/(index.html)に変更していきます。


## 実装

それぞれのファイルを編集していきます。

### firebase.json(rewrite設定)

まず、firebase.jsonのrewritesを設定して全て(**)のリクエストをfunction, appに転送します。


```firebase.json
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "app"
      }
    ]
  }
}
```

### SPAをホスティング

認証を設定する前に、そもそもSPAアプリをfunction経由で表示する設定をします。
大きくは２つの設定を。

```index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');


const app = express();

//とりあえずスタティックの場所指定
app.use(express.static(__dirname + '/react-app/build/'));

//それ以外の時もindex.htmlへ
app.all('*', (req, res, next) => {
    res.sendFile(path.resolve(__dirname, 'react-app/build/index.html'));
})

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

### deploy

一旦ここでdeployしておきます。

```bash
firebase deploy
```

reactの標準画面が見えればOKです。
一見、Hostingを利用しているのと変わりませんが、functions経由で表示されています。


### Basic認証

Basic認証を設定します。まずモジュールをインストールします。

```bash
npm isntall --save basic-auth-connect
```

ここではID=admin, password=passwordに設定しています。

```diff:index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
+const basicAuth = require('basic-auth-connect');

const app = express();

+app.use(basicAuth('admin','password'));

//とりあえずスタティックの場所指定
app.use(express.static(__dirname + '/react-app/build/'));

//それ以外の時もindex.htmlへ
app.all('*', (req, res, next) => {
    res.sendFile(path.resolve(__dirname, 'react-app/build/index.html'));
})

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

```
firebase deploy
```

### IP制限

ついでにIP制限をも追加設定してみます。

```diff:index.js
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const basicAuth = require('basic-auth-connect');
+const requestIp = require('request-ip');

//許可IP 43.233.87.238
+const allowedIps = ['43.233.87.238'];

const app = express();

//basic認証
app.use(basicAuth('admin', 'password'));

//IPチェック
+app.all('*', (req, res, next) => {
+    //IPチェック
+    const clientIp = requestIp.getClientIp(req);
+    const isAllowed = allowedIps.indexOf(clientIp) !== -1;
+    if (!isAllowed) {
+        res.send("You can't access this site.")
+    } else {
+        next();
+    }
+})

//とりあえずスタティックの場所指定
app.use(express.static(__dirname + '/react-app/build/'));

//それ以外の時もindex.htmlへ
app.all('*', (req, res, next) => {
    res.sendFile(path.resolve(__dirname, 'react-app/build/index.html'));
})

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

## 参考

* https://gist.github.com/ryanoglesby08/1e1f49d87ae8ab2cabf45623fc36a7fe