フロントエンドの地獄
cover image

Firebase FunctionのCallable Functionsを使わずWebフロント側のAPIに寄せると管理が楽だよ

Date
2021/12/4
Tags
Firebase
技術
ペライチアドベントカレンダーの4日目です!
 
今回は表題にある通り、WebアプリではFirebaseのCallable Functions相当のものを自作すると楽な場合があるのでやってみましょうというエントリーです。

背景

今回の背景としては
  1. Firebase Auth、Firestoreを利用している
  1. 静的サイトでなくNext.jsなどを利用してSSRしていて、APIとWebフロントエンドを一緒に管理している。できる状態である
  1. FirestoreをSDK経由でなくデータを操作したい場面が存在する(firebase-adminを利用)
という場合の話です。
 
Firebaseを利用していて、FirestoreのRulesが複雑化する場合やロジックをクライアントサイドに露呈させたくない場合には、一般的にはFirebase FunctionsのCallable Functionsというものを利用します。(内部でFirestoreのルールを無視して実行できるfirebase-adminを利用します。)
Callable FunctionsはSDKを通すことで自動的に認証情報を付与した状態でFirebase Functionを外部から実行できる仕組みです。その後コード内で受け取った認証情報をもとに実行権限の有無などを確かめてから実行することが可能になります。
 

モチベーション

Callable Functionsはこれでとても便利なのですが、Firebase Functionsはデプロイ時にStorageにアップロードする関係などで、数が増えると全体のデプロイに時間がかかるようになります。(もちろん個別にもデプロイはできますがCIで管理等するとまたその管理も発生します。)
また、Webアプリケーションを構築する際にはAPIをWebフロントエンド側で作成すると、Firebase Functionsとのコードやデプロイプロセスが2重管理にならないため、できるだけWebフロントエンド側に寄せたほうが楽になります。
これによりFirebase Functionsとアプリケーションのデプロイが同時にならないことのために、バージョンを見て内容を変えるような実装なども必要もなくなります。
 
といった苦労から開放されるために「Callable Functionsの認証周りを自前実装してWebアプリケーション側のAPIに組み込んでしまおう」という内容になります。といってもFirebase SDKに元々そういったことを行うための機能が組み込まれているため実装は非常に簡潔です。
(前置きが長くなってしまってすいません!笑)
 
さぁお待ちかねの実装です。

クライアントサイドの実装

 
クライアントサイドでは認証情報をAPIに渡すため、ログインユーザーのJWTを発行し、それを付与してAPIにポストするだけです。
const token = await firebase.auth().currentUser.getIdToken();
const res = await fetch(`/api/createUser`, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({id: "nabettu", name:"なべっつ"}),
});
JavaScript
これで api/createUser にデータをポストすることができたので、サーバーサイドを見てみます。

サーバーサイドの実装

authenticateという関数で認証しているかチェックしつつrequestにuser情報を添付します。
今回のアプリケーションではメールアドレス認証が済んでいるユーザーかどうかもついでにチェックします。
/*
	ユーザーの検証と、メールアドレス認証が完了しているかどうかのチェック
*/
export const authenticate = async (req, res, next) => {
  if (
    !req.headers.authorization ||
    !req.headers.authorization.startsWith("Bearer ")
  ) {
    res.status(403).send("Unauthorized");
    return;
  }
  const idToken = req.headers.authorization.split("Bearer ")[1];
  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken);
    if (decodedIdToken?.email_verified) {
      req.user = decodedIdToken;
      next();
      return;
    } else {
      res.status(403).send("not verified email user. Unauthorized");
      return;
    }
  } catch (e) {
    res.status(403).send("Unauthorized");
    return;
  }
};

const createUser = (req, res) => {
  try {
    authenticate(req, res, async () => {
			// 認証済のユーザー情報
			console.log(req.user)
			// postされたデータ
			const newUserId = req.body.id;
		})
	}
}
JavaScript
 
APIごとに作る必要はなく、authenticate関数を共通化してAPIに挟むだけで認証済かどうか、また、ユーザー情報が添付されてた状態でコードを実行できるので非常に簡単です。
例えば今回の例ですと newUserId をみて「ユーザーごとに1人1つだけ特定のコレクション配下に指定IDのデータを追加したい」などの検証をサーバーサイドで実行できます。
これによってFirestore Rulesが複雑化することも防げますし、APIとフロントのコードが一緒にデプロイされるのでバージョン分け等の苦労もなくなります。
 
以上、SSRするWebアプリ(APIとフロントのコードが一緒になっている)場合Callable Functionを使わずAPIとしてフロントコードと一緒にすると楽になるというお話でした〜
 
引き続きペライチアドベントカレンダーをよろしくお願いします!
明日はNagamiさんです!