Cloudflare Workers でも Firebase Authentication を使えるぞ!!
Cloudflare Workers では KV だったり Durable Objects や R2 などといった外部ストレージへアクセスをして何かしら操作するようなプログラムを動かすことができます。しかし、誰でもその操作ができてしまうとセキュリティ面や使用料の面で問題が発生します。
interface Env {
ANYBUCKET: R2Bucket
}
// 誰でもファイルアップロードできちゃう Worker :pien:
export default {
async fetch(request: Request, env: Env) {
const formdata = await request.formData()
const imagedata = formdata.get("imagedata")
if (imagedata === null) {
throw new Error("not found imagedata")
}
const file = imagedata as File
const content = await file.arrayBuffer()
const hashedKey = generateHashedKey(content)
// R2 Storage へアップロード
await env.ANYBUCKET.put(hashedKey, content)
return new Response(`https://${hostname}/${hashedKey}}`, {
headers: { 'content-type': 'text/plain' },
})
}
}
こういった問題を防ぐためにリクエストを許可して良いか判断するための認可処理を挟むでしょう。その手段の一つとして Firebase Authentication を使った方法を考えました。
Cloudflare Workers へデプロイできるコード量には上限があるため、基本的には外部ライブラリに依存しない形で実装しようと色々コードを書いていたのですが、まあまあ考えるべきことがあってライブラリにしたほうが良さそうだなと思い作成しました。
npm registry からインストールできます。
$ npm i firebase-auth-cloudflare-workers
考慮した点を紹介したいのですが、まずはこのライブラリを使うとどんな感じで認可処理を挟めるか見てみましょう。ここでは推奨[1]されている Module Worker Syntax で記述します。
import type { EmulatorEnv } from "firebase-auth-cloudflare-workers";
import { Auth, WorkersKVStoreSingle } from "firebase-auth-cloudflare-workers";
// ① ライブラリで提供されている EmulatorEnv を extend する
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, "")
// ② ライブラリで提供される API を利用するために初期化
const auth = Auth.getOrInitialize(
env.PROJECT_ID,
WorkersKVStoreSingle.getOrInitialize(env.PUBLIC_JWK_CACHE_KEY, env.PUBLIC_JWK_CACHE_KV)
)
// ③ リクエストされた JWT を検証する
return await auth.verifyIdToken(jwt, env)
}
この関数を用意できると最初で提示したコードへ簡単に認可処理を挟むことが可能になります。
export default {
async fetch(request: Request, env: Env) {
+ const verified = await verifyJWT(request, env)
+ if (verified === null) {
+ return new Response(null, {
+ status: 400,
+ })
+ }
const formdata = await request.formData()
const imagedata = formdata.get("imagedata")
if (imagedata === null) {
throw new Error("not found imagedata")
}
さて、お気づきな点がいくつかあるかもしれませんがライブラリにしたほうが良いと思った、まあまあ面倒だった考慮したポイントをいくつか紹介します。
①
Cloudflare Workers はローカルでも実行しながら開発することが可能です。そうなると Firebase Authentication Emulator も使いながら開発したくなります。そこで、本家 SDK と同様 FIREBASE_AUTH_EMULATOR_HOST
が存在していればローカルモードとして検証を行うようにしました。
②
環境変数はリクエスト内で拾う必要があります。[2]これだとリクエストごとに Auth
オブジェクトの初期化を行う必要が出てきてパフォーマンスに影響します。そこでシングルトンオブジェクトとして提供することにしました。
そして検証では Google が配布しているエンドポイントから公開鍵を取得して、それを利用する必要があります。[3]リクエストごとに取得して検証するのは非効率なので、ライブラリでは Cloudflare KV へ公開鍵をキャッシュできる仕組みを提供しています。[4]しかし、ここでも環境変数で得た情報を基にオブジェクトを生成したいケースがありそうだったため、Auth
同様にシングルトンオブジェクトとすることで一度だけ初期化処理を行うようにしています。
ここでは Cloudflare KV をキャッシュ用のストレージとして利用していますが、好きなストレージを利用できるように作ってます。[5]
③
ここでは取得してきた公開鍵とリクエストで送られてきた JWT の検証を行います。単純に検証するのではなく、Firebase のドキュメントで定められた「ID トークン ヘッダーの要件」と「ID トークン ペイロードの要件」を満たすかどうかも確認する必要もあります。これを満たしていればペイロードには様々な情報を含めることが可能です。[6]
ここでちょっとだけ JWT の話をします。JWT は {header}.{payload}.{signature}
の構成になっており、それぞれのパートは base64 URL でエンコードされた JSON です。
Cloudflare Workers の世界では Web Standard API を利用する必要があります。そしてなんと、btoa という base64 encode 可能な API も提供されています!...が、ご存知の通り、これは UTF-8 に対応していないため色々頑張って encode しないと下記のようにエラーになってしまいます。
そういった、JWT を検証するために必要な基本的な部分もがんばらないといけませんでした。しかし、Crypto.subtle (SubtleCrypto) という署名(signature の部分)の検証に必要な API が提供されてたため、その点は便利でした。
まとめ
ゼロ依存として作成したせいで、色々考慮すべき点があり、最終的にはライブラリとして提供していたほうが良さそうだと思いライブラリを作成しました。その考慮した点をいくつか紹介しました。
これは個人的な気持ちですが、いくつかの Edge 環境で動くフレームワークである hono のミドルウェアも提供できると良いなと思ってます。
このライブラリを作成している過程でちょっとした裏技?も見つけたので記事へのリンクも張っておきます。
-
Service Worker Syntax だと環境変数はグローバル変数としてアクセスできます。 ↩︎
-
https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library ↩︎
-
https://github.com/Code-Hex/firebase-auth-cloudflare-workers#workerskvstoresinglegetorinitializecachekey-string-cfkvnamespace-kvnamespace-workerskvstoresingle ↩︎
-
https://github.com/Code-Hex/firebase-auth-cloudflare-workers#authgetorinitializeprojectid-string-keystore-keystorer-auth ↩︎
-
https://firebase.google.com/docs/auth/admin/custom-claims?hl=ja ↩︎
Discussion