Node.js
Firebase
cloudfunctions
FirebaseHosting
OriginalLIFULLDay 16

Firebase HostingにIPでアクセス制限をかける。そして少し妥協する。

本記事はLIFULL Advent Calendar 2017の16日目の記事です。

昨日は@watanataの「passenger-statusを監視するMackerelのプラグイン」でした。

Firebase Hostingにアクセス制限かけたい

ご存知の方も多いと思いますが、FirebaseにはWEB Hostingの機能が無料で公開されています。
デプロイの手順もCLIツールでかんたんにでき、公開されたWEBページはhttpsになっているという大変ありがたい機能です。サクッと作ったWEBを公開するのにもってこいですね。

しかしこのサービスにはアクセス制限の機能がなく、一度deployしてしまうと全世界に配信されてしまいます。
もちろん本番サイトであればそれで良いのですが、開発環境や社内ツールとして利用したい場合1 にはアクセスを制限できないのは少し困ります。

そこで、今回は今年のGoogle I/Oで発表されたCloud FunctionsとFirebase Hostingを組み合わせた動的コンテンツ配信機能を利用して、IPアドレスによるホワイトリスト形式のアクセス制限をかけてみます。

プロジェクトのセットアップ

Firebase HostingとCloud Functionsの設定をしていきます。
とはいえ、firebaseはCLIツールがとても使いやすく、ほとんど公式ドキュメントに書いてあるとおりにすすめていけば問題なく導入できますので、ここではhistoryだけ貼っておきます。

# firebaseのcliツールをnpm経由でインストール
$ npm install -g firebase-tools

# 初期化
$ firebase init
:
略
:

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices. Functions: Configure and deploy Cloud Functions, Hosting: Configure
and deploy Firebase Hosting sites

=== Project Setup

:
略
:

? Select a default Firebase project for this directory: [create a new project]

=== 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? TypeScript
? Do you want to use TSLint to catch probable bugs and enforce style? Yes

:
略
:

? Do you want to install dependencies with npm now? Yes

:
略
:

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
:
略
:

# 作ったプロジェクトを利用するように宣言
$ firebase use --add your-project

IP制限をかける

準備が終わったらさっそく制限をかけていきましょう。
functions/src/index.ts を編集していきます。JSを設定した人はいい感じに読み替えてください。

実装

functions/src/index.ts
import * as functions from 'firebase-functions';
import * as requestIp from 'request-ip';
import * as fs from 'fs';

exports.filter = functions.https.onRequest((req, res) => {
  const clientIp   = requestIp.getClientIp(req);

  const isAllowed  = ['172.0.0.1'].indexOf(clientIp) !== -1;
  const statusCode = isAllowed ? 200 : 400;
  const filename   = isAllowed ? 'index' : '400';

  const html = fs.readFileSync(`./templates/${filename}.html`).toString();

  res.status(statusCode).send(html);
});

これだけです。かんたんですね。
かんたんすぎてTypeScriptっぽいところがほとんどない。

[172.0.0.1]の部分を許可したいIPにすることで無事IP制限が実現できます。
templates/${filename}.htmlの部分はいい感じのHTMLを置いておきましょう。

解説

functions.https.onRequestexpressフレンドリーに設計されており2、 expressに対応しているライブラリであればほとんどそのまま利用が可能になっています。
そこで今回はexpressのRequestオブジェクトからIPアドレスを取得してくれるrequest-ipを利用してアクセスしてきたIPを取得しています。

あとはとってきたIPを使ってステータスコードと表示するhtmlファイルを切り替えるだけでおしまい。

設定ファイル

実装が終わったら設定ファイルの変更です。
Hostingされたサイトにリクエストがきたら今回作成したCloud Functionsを呼び出すようにしていきます。
私と同じように初期化した人は下記のように修正をすればOKです。

firebase.json
{
  "functions": {
    "predeploy": "npm --prefix functions run build"
  },
  "hosting": {
    "public": "public",
+    "rewrites": [ {
+      "source": "**", "function": "filter"
+    } ],
+    "redirects": [ {
+      "source" : "/",
+      "destination" : "/app",
+      "type" : 301
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

解説

rewritesの設定により、すべてのリクエストを今回作成したfilterの結果を返すように書き換えます。
(ドキュメントにあるサンプルとほぼおなじなので解説不要だと思いますが)
これでもう設定は終わりかと思いきや、実はこの設定には一点問題が有ります。

それが / へのリクエストです。
ここに関してはrewritesが働かず、必ずpublic/index.htmlを返すようになっています。
つまり/へのアクセスはそもそも動的コンテンツを返せないのです。

悲しい。

そこでredirectsの修正が必要になります。ここが妥協点です。
/にアクセスがきたとき、どこでもいいので(この例では/app)リダイレクトをかけてやることで絶対に/にアクセスできないようにします。
せっかくなので恒久的リダイレクトにしてやりましょう。
こうすることで実質的にはすべてのページにアクセス制限をかけることが可能になります。
かけられたということにしてほしい。

デプロイ

$ firebase deploy
:
略
:
Hosting URL: https://{your-project}.firebaseapp.com

これで作成したアプリケーションがWeb Hostingに公開されます。

せっかくなので、今回僕が作成したサンプルアプリケーションを公開しておきます。
よろしければ参考にしてください。

https://access-policy-sample.firebaseapp.com/

/appにリダイレクトされて、「Access Denined」と表示されましたか?
僕にはindexページが表示されています。
どうか、参考にしてください。

もしみなさんにAccess Denined以外のページが見えていたら、僕は怖いです。
どうか、どうか参考にしてください。


明日は@hanaisの記事です。お楽しみに。


  1. 本記事はLIFULL Advent Calendarですが、今回の実装を社内ツールとして利用しているわけではありません。 

  2. というか、内部的にexpressが利用されていそうです。functions.https.onRequestの返り値はexpressにおけるミドルウェアという立ち位置です。コードを見る限り。