JavaScript
Node.js
Firebase

Firebaseの3機能を連携して可能性を探る(Hosting + Functions + Authentication)

やること

Firebaseの3つの機能 Hosting(静的サイトのサーブ)、Auth(認証機能)、Functions(マイクロサービス)を連携させ、その可能性を探る。

  • Firebase のHosting で静的サイトを公開するが、
  • 特定のパス (/hello) は動的コンテンツであり、Firebase Functions の関数 helloWorld にディスパッチする
  • helloWorld は、 Firebase Auth と連携し認証ユーザに応じたレスポンスを返す
  • index.html では Firebase Auth を使ったサインイン/サインアウトを実装するとともに /hello にリクエストを投げるI/Fを付ける

前提

  • firebase-tools をインストールして firebase deploy で公開できる用意ができていること
  • 私の機能の記事 などを参考に、認証ユーザを事前に作成しておくこと。

スクリーンショット(index.html)

無題.png

01. Functions の作成

helloWorld を作ろう。リクエストのAuthorizationヘッダからトークンを抜き出し、Firebase Auth の機能でトークンのベリフィケーションを実施。成功したら、ユーザーIDを含む文字列を返却する。

functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();


function tokenOf (req) {
  if (!req.get('Authorization')) { return null; }
  const x = req.get('Authorization').split('Bearer ');
  return x.length === 2 ? x[1]: null;
}

exports.helloWorld = functions.https.onRequest((req, res) => {
  const token = tokenOf(req);
  if (!token) { res.status(401).send('not sign in'); }
  return admin.auth().verifyIdToken(token)
    .then(decoded => res.status(200).send(`hello ${decoded.uid}`))
    .catch(err => res.status(401).send(err));
});

02. Functionsをルーティング

firebase.json にURLパスと、Functionsの対応関係をかく。rewrites フィールドに注目。これで、URL /hello へのアクセスが、 helloWorld にディスパッチされる。

firebase.json
{
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint"
    ]
  },
  "hosting": {
    "public": "public",
    "rewrites": [ { "source": "/hello", "function": "helloWorld" }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

03. 認証機能付き静的ページの作成

defer を付けて非同期ロードでも順序は守って実行。自動生成してくれる init.js の中でSDKの初期化がされる。
ロジックは全部 main.js に書こう。

public/index.html
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script defer src="/__/firebase/5.0.2/firebase-app.js"></script>
<script defer src="/__/firebase/5.0.2/firebase-auth.js"></script>
<!--
<script defer src="/__/firebase/5.0.2/firebase-database.js"></script>
<script defer src="/__/firebase/5.0.2/firebase-messaging.js"></script>
<script defer src="/__/firebase/5.0.2/firebase-storage.js"></script>
-->
<script defer src="/__/firebase/init.js"></script>
<script defer src="main.js"> </script>
</head>

<body>
<input id="userID" type="text" value=""/>
<input id="userPass" type="password" value=""/>
<button id="signInBtn">SignIn</button>
<button id="signOutBtn">SignOut</button>
<button id="hello">InvokeHello</button>
</body>
</html>

以前にサインインしていれば、明示的なサインインsignInWithEmailAndPasswordをしなくてもよい。observeAuthState()のような実装で、DOMを書き換えてサインインをしなくてもよいことを知らせるなどの工夫をするのがよさそう。hello()の実装のように、currentUserが非null ならトークンがゲットできるはずなので、それをヘッダにセットして、Functions にディスパッチされるURL /hello にリクエストを投げる。

public/main.js
function log (msg) {
  console.log(msg);
}

function observeAuthState () {
  firebase.auth().onAuthStateChanged((user) => {
    if (user) {
      log(`sign in as ${user.uid}.`);
    } else {
      log(`sign out.`);
    }
  });
}

function signIn () {
  const email = document.getElementById('userID').value;
  const password = document.getElementById('userPass').value;
  firebase.auth().signInWithEmailAndPassword(email, password)
    .then(() => log('login sucess'))
    .catch(log);
}

function signOut () {
  firebase.auth().signOut();
}

async function hello () {
  const me = firebase.auth().currentUser;
  if (!me) { log('not signed in.'); return; }
  const token = await me.getIdToken();
  const resp = await fetch('/hello', { headers: { 'Authorization': `Bearer ${token}` } });
  resp.text().then(log);
}

// Listener
document.addEventListener('DOMContentLoaded', observeAuthState);
document.getElementById('signInBtn').addEventListener('click', signIn);
document.getElementById('signOutBtn').addEventListener('click', signOut);
document.getElementById('hello').addEventListener('click', hello);

まとめ

Firebase まじですごい!