今個人で作ってるアプリの 認証 + Graphql の部分を抜き出して GitHub に公開した。

mizchi/next-boilerplate-20200727

next.js + vercel + firebase は (パーツを良く選べば) 最高

next.js はルーティングを持つページを作るには最高で、サーバー、静的サイト、JAM スタック、AMP と必要に応じて選択できる。React ベースならこれ一択。

認証サーバーの実装は毎度疲れるし、Firebase Athunetication はこの点においては OAuth Secret を置くだけ + Custom Provider も作れるので、最高。

それと比べて firestore は、ちょっと前に firestore べったりでアプリを試作したことがあったのだが、型がないためにかなり扱いづらく、また読み書きの速度が遅くパフォーマンスも出なかったので、次に作るときはリアルタイム性が求められる部分だけ firestore にして、それ以外は他の DB で作ろうと決めた。

自分の手元では mongodb(atlas) を使ってるが、その部分は公開していない。とりあえず typegoose が便利。

サーバー実装を公開しなかった理由だが、 graphql をクラサバの分解点にしたので、 Firebase の存在をフロントエンドから隠蔽するために の記事にあるような分解点を簡単に作れて、GraphQL のスキーマに従えば、サーバー越しにバックエンドはなんでもいい状態にした。その上で Graphql のリゾルバの実装で、Firestore を使うかどうかは別途考えればよい。サーバーだと admin 権限を持ってるので、firebase rules でクライアントでは禁止した書き込みもできる。

vercel の実体はおそらく AWS Lambda なので、Firebase のある GCP とつなぐにはパフォーマンス面で不利なのだが、認証はそもそも重い操作なのと、JWT の検証だけならコネクション無しで済ませられるので、ここは採用した。とりあえずここが新プロジェクトを作る際のスタートラインになる、というラインで切り出した。


使い方

こまかいのは README を見て。

要約

  • Firebase Authentication で認証を行う
  • Authorization ヘッダに Auth 情報の JWT トークンを付与して送信
  • Graphql サーバーで JWT トークンをデコードしてユーザーを検証
  • Graphql Codegen で型定義 + hooks api を生成して、それをクライアントから使う

準備

  • firebase のプロジェクトを登録
  • firebase の設定を src/config/firebase.json に置く
  • firebase admin の key を生成して、 src/config/firebaseAdmin.json に置く
  • 使いたい firebase authentication の provider を有効にする
    • このとき GitHub なら GitHub の OAuth App を登録しておく

認証

  • Firebase Authentication で認証
  • 認証後、クライアントからは firebase auth で生成した JWT token を Authorization ヘッダに付ける (RFC6750)

Graphql API

  • /api/graphql に Vercel の Function でデプロイ
  • Graphql のスキーマを書いて、コードを生成する
  • サーバー: 生成された型に沿って graphql-server を実装する
  • クライアント: 自動生成された hooks でクライアントを実装

参考


実装詳細

firebase authentication には、JWT トークンを生成する機能があり、クライアントで生成、サーバーサイドでは firebase-admin で verify する。

クライアント

import ApolloClient from "apollo-boost";
import { getAuth } from "./firebaseHelpers";

export function createGraphqlClient() {
  const client = new ApolloClient({
    uri: "/api/graphql",
    async request(operation) {
      const auth = getAuth();
      if (auth && auth.currentUser) {
        try {
          const token = await auth.currentUser.getIdToken(true);
          operation.setContext({
            headers: {
              authorization: token ? `Bearer ${token}` : "",
            },
          });
        } catch (err) {}
      }
    },
  });
  return client;
}

サーバー

// ...
const apolloServer = new ApolloServer({
  typeDefs,
  // @ts-ignore
  resolvers,
  async context(args) {
    const { req } = args;
    const idToken = getIdTokenFromReq(req);
    if (idToken != null) {
      const decoded = await verifyIdToken(idToken);
      return { idToken: decoded };
    }
    return { idToken: null, decoded: null };
  },
});
History
4ef23b0 - Update Tue Jul 28 16:02:00 2020 +0900