たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Firebase FunctionでJWTを作成して、バックエンドへのリクエストの際に使用する簡単なやり方

前提

NestJSプロジェクトの作成方法や、PassportとJWTを組み合わせて認証機構を実装する方法に関してはこちらを参照。

takagimeow.hatenablog.com

Firebase プロジェクトの作成方法についてはこちらの記事を参照。

takagimeow.hatenablog.com

Firebase FunctionsにVitestを導入する方法についてはこちらの記事を参照。

必要なパッケージをインストール

JWTを作成するためにjsonwebtokenを入れる。

yarn add jsonwebtoken

テスト時に作成したjwtをデコードしたいので、jw t-decodeパッケージを入れる。

yarn add -D jwt-decode

Firebase Functionsでfetchを使用したいので、node-fetchを入れる。

yarn add node-fetch

JWT文字列を生成する関数を作成する

createJwtという関数を作成する。

jsonwebtokensign関数は、渡したペイロードを同期的にJSON Web トークン文字列に署名する関数である。

オプションで指定したalgorithm: "HS256"は、HS256アルゴリズムを使用して署名するという意味である。

// src/utils/createJwt.ts

import {sign} from "jsonwebtoken";

export function createJwt(
    payload: Record<string, string | number>,
    secret: string
) {
  const token = sign(payload, secret, {
    algorithm: "HS256",
  });
  return token;
}
HS256とは

対象アルゴリズムと呼ばれていて、ひとつの鍵を署名側と検証側で共有するアルゴリズムである。なのでNestJSで使用しているシークレットとFirebase Functions側で同じ値を環境変数などを通して保有しておく。

RS256 と HS256 ってなにが違うの - Qiita

createJwt関数に対するテストを作成する

先ほど作成したcreateJwt関数に対して軽いテストを作成してみる。 作成したトークンが正しくデコードできて、その内容が元のペイロードと同じ内容を含んでいるかを試すだけの簡単なテスト。

// src/utils/createJwt.spec.ts

import {test, expect} from "vitest";
import jwtDecode from "jwt-decode";
import {createJwt} from "./createJwt";

// 環境変数と見立てる
const JWT_KEY = "secretKey";

test("smoke test", async () => {
  // payloadを準備する
  const payload = {
    userId: "569125ee-56f8-4955-937c-70a5fa8ababc",
    exp: 1657168882,
  };
  const result = createJwt(payload, JWT_KEY);
  const decodedPayload = jwtDecode<typeof payload>(result);
  expect(result).toBeTruthy();
  expect(decodedPayload.userId).toBe(payload.userId);
});

呼び出し可能関数を実装してみる

バックエンドで動いているNestJSサーバーに対してGETリクエストを送って受け取ったデータをそのままクライアントに返す感じ。

対象ユーザーのUIDはそのままcontext.auth.uidを使用する。これをもとにJWT文字列を作成して、ヘッダーに設定した上でリクエストを送信する。

実際に作成する場合は、送られてきたuidのユーザーがFirebase Authenticationに登録されているかを確認したり、バックエンドにも問い合わせたりなどの処理を追加した方が良いかもしれない。

// src/routes/tab-one/activities/getActivities.ts

import type {CallableMethod} from "~/@types/callableMethod";
import {createJwt} from "~/utils/createJwt";
import fetch from "node-fetch";

type Activity = {
  id: string;
  label: string;
  date: string;
  value: string;
  method: "COUNT" | "TIME";
  exp: number;
  createdAt: string;
  icon: string;
};

// eslint-disable-next-line @typescript-eslint/ban-types
export type RequestData = {};
export type ResponseData = {
  formError?: string;
  activities?: Activity[];
};

export const getActivities: CallableMethod = async (
    data,
    context
): Promise<ResponseData> => {
  // uidの有無を確認する
  if (!context.auth?.uid || context.auth.uid === "") {
    const result: ResponseData = {
      formError: "uidを取得することができませんでした",
    };
    return result;
  }

  const key = "secretKey";
  const payload = {
    sub: context.auth.uid,
  };
  const token = createJwt(payload, key);
  const response = await fetch(
      `http://localhost:3000/users/${
        payload.sub
      }/activities?method=year&base_date=${new Date().getTime()}`,
      {
        method: "GET",
        headers: {
          Accept: "application/json",
          Authorization: `Bearer ${token}`,
        },
      }
  );
  const latestActivities = await response.json() as Activity[];
  return {
    activities: latestActivities,
  };
};

呼び出し可能関数用のテストを作成してみる

先ほど作成したgetActivities関数を実際に呼び出してみて、結果が帰ってくるかの簡単なテストを行なってみる。

※先にNestJS側でサーバーを起動したり、必要なデータをシードしておくこと。

// src/routes/tab-one/activities/getActivities.spec.ts

import * as firebaseFunctions from "firebase-functions";
import firebaseFunctionsTest from "firebase-functions-test";

import {getActivities, RequestData} from "./getActivities";
import {WrappedFunction} from "firebase-functions-test/lib/v1";

let wrapped: WrappedFunction<RequestData>;

beforeAll(async () => {
  const {wrap} = firebaseFunctionsTest();
  // テスト対象の関数をラップする
  wrapped = wrap(firebaseFunctions.https.onCall(getActivities));
});

afterAll(() => {
  vi.clearAllMocks();
});

test("smoke test", async () => {
  expect(1).toBe(1);
  const userId = "09ed994e-7ca0-4d9f-a1a5-c7d24ada765a";
  // 実行する
  const result = await wrapped({}, {auth: {uid: userId}});
  expect(result.activities.length).toBe(4);
});

今回は内部でFirebase AuthenticationやFirestoreを使用していていないので、エミュレーターは使用しないので、そのままvitestを実行するだけで良い。

yarn vitest

もし使う場合は次のコマンドを使用する。yarn testを実行するために"test": "vitest"スクリプトをpackage.jsonに追加しておく。

firebase emulators:exec --project project-id --only auth 'yarn build && yarn test'