Cloudflare WorkersでFirebase Authのトークン検証を実装する

Cloudflare WorkersでFirebase Authのトークン検証を実装する
Photo by King's Church International / Unsplash

今とあるアプリを新しく作っています。

構成はこんな感じです。

アプリ:Expo
APIサーバー:graphql-yoga on Cloudflare Workers
認証:Firebase Authentication
DB:Neon

このとき、APIをフルオープンにすると問題があるので制限を設けたいです。

ということで、サクッとfirebase-adminを使ってみたんですが、Cloudflare Workersでは動きませんでした。

firebase-adminを使って実装するとUncaught TypeError: globalThis.XMLHttpRequest is not a constructorとエラーが出ます。

✘ [ERROR] A request to the Cloudflare API (/accounts/hogehoge/workers/scripts/fugafuga) failed.

  Uncaught TypeError: globalThis.XMLHttpRequest is not a
  constructor
    at null.<anonymous>
  (file:///Users/owner/Library/pnpm/global/5/.pnpm/[email protected]/node_modules/rollup-plugin-node-polyfills/polyfills/http-lib/capability.js:20:11)
  in checkTypeSupport
    at null.<anonymous>
  (file:///Users/owner/Library/pnpm/global/5/.pnpm/[email protected]/node_modules/rollup-plugin-node-polyfills/polyfills/http-lib/capability.js:39:45)
  in
  ../../../../../Library/pnpm/global/5/.pnpm/[email protected]/node_modules/rollup-plugin-node-polyfills/polyfills/http-lib/capability.js
    at null.<anonymous> (worker.js:9:58) in __init
    at null.<anonymous>
  (file:///Users/owner/Library/pnpm/global/5/.pnpm/[email protected]/node_modules/rollup-plugin-node-polyfills/polyfills/http-lib/request.js:1:1)
  in
  ../../../../../Library/pnpm/global/5/.pnpm/[email protected]/node_modules/rollup-plugin-node-polyfills/polyfills/http-lib/request.js
    at null.<anonymous> (worker.js:9:58) in __init
    at null.<anonymous> (node-modules-polyfills:http:30:1)
  in node-modules-polyfills:http
    at null.<anonymous> (worker.js:9:58) in __init
    at null.<anonymous>
  (node-modules-polyfills-commonjs:http:2:18) in
  node-modules-polyfills-commonjs:http
    at null.<anonymous> (worker.js:12:53) in __require
    at null.<anonymous>
  (file:///Users/owner/Projects/deg84/fugafuga/node_modules/firebase-admin/lib/utils/api-request.js:23:14)
  in
  ../../node_modules/firebase-admin/lib/utils/api-request.js
   [code: 10021]

  
  If you think this is a bug, please open an issue at:
  https://github.com/cloudflare/workers-sdk/issues/new/choose

なので、firebase-adminは使えません。

ちなみにgraphql-yogaには@graphql-yoga/plugin-jwtというプラグインがありますが、firebase-adminと同じようにエラーになってしまいます。

調べてみるとCode-Hex/firebase-auth-cloudflare-workersが使えそうでした。

GitHub - Code-Hex/firebase-auth-cloudflare-workers
Contribute to Code-Hex/firebase-auth-cloudflare-workers development by creating an account on GitHub.

Zennにも記事がありました。

Cloudflare Workers でも Firebase Authentication を使えるぞ!!

なんですが、バージョンが2.0系になっていて若干情報が古くそのままでは動かなかったので、2024年3月末時点での状態を記録しておこうと思います。

前提

  • Cloudflare Workersの設定はwrangler.tomlで行ってます
  • ローカルで動かすときにもFirebaseのエミュレーターは使ってません。エミュレーターを使う場合は適宜FIREBASE_AUTH_EMULATOR_HOSTの設定が必要です
  • Module Worker syntaxを使ってます(推奨なので)

wrangler.tomlの内容

main = "src/main.ts"
compatibility_date = "2024-03-26"
workers_dev = false

[env.dev]
name = "hoge-dev"
kv_namespaces = [
  { binding = "PUBLIC_JWK_CACHE_KV", id = "hogehogehoge" }
]
[env.dev.vars]
PROJECT_ID = "hoge-dev"
PUBLIC_JWK_CACHE_KEY = "hoge-dev-public-jwk-cache"

[env.staging]
name = "hoge-staging"
kv_namespaces = [
  { binding = "PUBLIC_JWK_CACHE_KV", id = "fugafugafuga" }
]
[env.staging.vars]
PROJECT_ID = "hoge-staging"
PUBLIC_JWK_CACHE_KEY = "hoge-staging-public-jwk-cache"

[env.production]
name = "hoge"
kv_namespaces = [
  { binding = "PUBLIC_JWK_CACHE_KV", id = "piyopiyopiyo" }
]
[env.production.vars]
PROJECT_ID = "hoge-prod"
PUBLIC_JWK_CACHE_KEY = "hoge-public-jwk-cache"

開発環境(ローカル)用の環境変数を[env.dev]に書いてます。wrangler dev --env devで起動しないと読み込まれないので注意です。

Worker KVはそれぞれの環境用に作成してください。

main.ts

import type {
  EmulatorEnv,
  FirebaseIdToken,
} from "firebase-auth-cloudflare-workers";
import { Auth, WorkersKVStoreSingle } from "firebase-auth-cloudflare-workers";
import { createSchema, createYoga } from "graphql-yoga";

interface Bindings extends EmulatorEnv {
  PROJECT_ID: string;
  PUBLIC_JWK_CACHE_KEY: string;
  PUBLIC_JWK_CACHE_KV: KVNamespace;
  FIREBASE_AUTH_EMULATOR_HOST: string;
}

const verifyJWT = async (
  req: Request,
  env: Bindings,
): Promise<FirebaseIdToken | null> => {
  const authorization = req.headers.get("Authorization");
  if (authorization === null) {
    return null;
  }
  const jwt = authorization.replace(/Bearer\s+/i, "");
  const auth = Auth.getOrInitialize(
    env.PROJECT_ID,
    WorkersKVStoreSingle.getOrInitialize(
      env.PUBLIC_JWK_CACHE_KEY,
      env.PUBLIC_JWK_CACHE_KV,
    ),
  );

  return await auth.verifyIdToken(jwt, false, env);
};

const yoga = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        hello: String!
      }
    `,
    resolvers: {
      Query: {
        hello: (_, _args, context) => {
          return `Hello, ${context.token?.name ?? "world"}!`;
        },
      },
    },
  }),
});

export async function fetch(
  req: Request,
  env: Bindings,
  ctx: ExecutionContext,
) {
  const token = await verifyJWT(req, env);
  if (token === null) {
    return new Response(null, {
      status: 400,
    });
  }
  return yoga(req, env, ctx, { token });
}

export default { fetch };

これで、リクエストのAuthorizationヘッダーのトークンをチェックするようになります。

リクエスト例

curl -H "Content-Type: application/json" -H "Authorization: Bearer hogehoge" -d '{"query": "query { hello }"}' http://localhost:8787/graphql

トークンは適宜正しいもの変更してください。

まとめ

Cloudflare Workers上でFirebase Authenticationを使う方法について、firebase-auth-cloudflare-workersライブラリを利用した実装方法を紹介しました。

リクエストのAuthorizationヘッダーに含まれるJWTトークンを検証し、認証済みのリクエストのみ許可するようにできました。

同じように困っている方の参考になれば幸いです。

Cloudflare Workers便利なので使っていきましょう!