0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare Workers + Hono で Firebase Auth認証を試したメモ

Last updated at Posted at 2025-01-04

概要

CloudflareでホスティングしているサイトでFirebase Authenticationを使って認証を行う。今回は匿名認証を試す。

ソースコード

フロー

Firebaseのプロジェクト作成

Firebaseは利用できるようになっている前提とする。*
Firebase Consoleから「プロジェクトを作成する」を選択。

image.png

プロジェクトを作成するまでの記録

image.png
image.png
image.png
新しいアナリティクスアカウントを作成。
image.png
image.png
image.png
image.png

Webアプリを登録してSDK用の設定を確認する

Firebase を JavaScript プロジェクトに追加する
image.png

image.png

image.png

匿名認証を有効化する

image.png

image.png
image.png

image.png

image.png

image.png

無料プランの制限

image.png

フロントエンド

環境変数の設定

image.png
image.png
image.png

環境変数は.envファイルの内容をコピペすると簡単に入力できる。すごい。

image.png

プレビューで確認したければ、環境を切り替えて同様に環境変数を設定する必要がある。
image.png

バックエンドとの通信のために、バックエンドのドメインを環境変数VITE_BACKEND_DOMAINに追加する。
プレビューとプロダクションで値は異なるはず。これに間違えてフロントエンドのドメインを指定したところ、405 Method Not Allowedのエラーが発生した。指定間違いに注意。
image.png

apps/frontend/src/shared/api/client.ts
import type { AppType } from '@odyssage/backend/index';
import { hc } from 'hono/client';
import { getIdToken } from '../lib/auth/firebaseAuth';
import { BACKEND_DOMAIN } from '../lib/config';

export const apiClient = hc<AppType>(BACKEND_DOMAIN, {
  headers: async () => ({ Authorization: `Bearer ${await getIdToken()}` }),
});
export type ApiClient = typeof apiClient;
apps/frontend/src/shared/lib/auth/firebaseAuth.ts
import {
  getAuth,
  NextOrObserver,
  onAuthStateChanged,
  signInAnonymously,
  User,
} from 'firebase/auth';
import { initializeApp } from 'firebase/app';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID as string,
};
const firebaseApp = initializeApp(firebaseConfig);
const auth = getAuth(firebaseApp);

export const signInAnonymous = async () => {
  const ret = await signInAnonymously(auth);
  return ret;
};
export const onAuthStateChangedListener = (callback: NextOrObserver<User>) =>
  onAuthStateChanged(auth, callback);
export const getIdToken = async () => {
  const user = auth.currentUser;
  if (user == null) {
    return null;
  }
  return user.getIdToken();
};
apps/frontend/entities/auth/model/authSlice.ts
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface AuthState {
  uid: string | null;
  displayName: string | null;
}

const initialState: AuthState = {
  uid: null,
  displayName: null,
};

export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    setUser(
      state,
      action: PayloadAction<{ uid: string | null; displayName: string | null }>,
    ) {
      state.uid = action.payload.uid;
      state.displayName = action.payload.displayName;
    },
  },
});

export const { setUser } = authSlice.actions;

const stateSelector = (state: RootState) => state[authSlice.reducerPath];

export const userDisplayNameSelector = createSelector(
  stateSelector,
  (c) => c.displayName,
);
export const uidSelector = createSelector(stateSelector, (c) => c.uid);
apps/frontend/entities/auth/service/loginAction.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import { apiClient } from '@odyssage/frontend/shared/api/client';
import {
  onAuthStateChangedListener,
  signInAnonymous,
} from '@odyssage/frontend/shared/lib/auth/firebaseAuth';
import { putHeaders } from '@odyssage/frontend/shared/lib/http/putHeader';
import { setUser } from '../model/authSlice';

export const loginAction = createAsyncThunk<
  void,
  void,
  { dispatch: AppDispatch; state: RootState }
>('loginAction', async (_, thunkAPI) => {
  onAuthStateChangedListener(async (user) => {
    if (!user) {
      thunkAPI.dispatch(setUser({ uid: null, displayName: null }));
      return;
    }
    thunkAPI.dispatch(
      setUser({ uid: user.uid, displayName: user.displayName }),
    );

    const result = await apiClient.api.user[':uid'].$get({
      param: { uid: user.uid },
    });
    if (result.status !== 404) return;
    console.log('Creating user');
    const ret = await apiClient.api.user[':uid'].$put(
      { param: { uid: user.uid } },
      { headers: putHeaders },
    );

    if (ret.status !== 204) {
      console.error('Failed to create user');
    }
  });
  await signInAnonymous();
});

Cloudflare設定

バックエンドでKVを使うため設定。
image.png
image.png
image.png

バックエンド

wrangler secret put FIREBASE_PROJECT_IDで環境変数にfirebaseのProjectIdを追加。
Hono FirebaseMiddlewareのメンテ頻度が少ないため、firebase-auth-cloudflare-workersを利用した。
firebase-adminだと、Uncaught TypeError: globalThis.XMLHttpRequest is not a constructorのエラーが出てしまうらしい。*

apps/backend/wrangler.toml
# 省略
[observability]
enabled = true

+ [vars]
+ PUBLIC_JWK_CACHE_KEY = "odyssage-public-jwk-cache"

+ [[kv_namespaces]]
+ binding = "PUBLIC_JWK_CACHE_KV"
+ id = "<先ほど設定したKVのid>"
apps/backend/.dev.vars
NEON_CONNECTION_STRING=postgresql://<DB_USER_NAME>:<DB_PASSWORD>@<NEON_DB_DOMAIN>.us-west-2.aws.neon.tech/main?sslmode=require
NEO4J_URL=neo4j+s://<NEO4J_INSTANCE_DOMAIN>.databases.neo4j.io
NEO4J_USER=neo4j
NEO4J_PASSWORD=<NEO4J_PASSWORD>

+ FIREBASE_PROJECT_ID=hogehoge
apps/bakend/src/utils/verifyJWT.ts
import { Auth, WorkersKVStoreSingle } from 'firebase-auth-cloudflare-workers';

import type { EmulatorEnv, FirebaseIdToken } from 'firebase-auth-cloudflare-workers';

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

export const verifyJWT = async (authorization: string | null | undefined, env: Bindings): Promise<FirebaseIdToken | null> => {
	if (authorization == null) {
		console.warn('No authorization header');
		return null;
	}
	const jwt = authorization.replace(/Bearer\s+/i, '');
	const auth = Auth.getOrInitialize(
		env.FIREBASE_PROJECT_ID,
		WorkersKVStoreSingle.getOrInitialize(env.PUBLIC_JWK_CACHE_KEY, env.PUBLIC_JWK_CACHE_KV),
	);

	return auth.verifyIdToken(jwt, false, env);
};
apps/backend/src/middleware/authorizeMiddlware.ts
import { createMiddleware } from 'hono/factory';
import { verifyJWT } from '../utils/verifyJWT';

// eslint-disable-next-line consistent-return
export const authorizeMiddleware = createMiddleware(async (c, next) => {
	const token = await verifyJWT(c.req.header('Authorization'), c.env);

	if (token == null) {
		return c.body('Unauthorized', 401);
	}

	await next();
});

apps/backend/src/route/index.ts
// 省略
const route = new Hono<Env>()
	.get('/', (c) => c.text('Hello Cloudflare Workers!'))
	.use('/user/*', authorizeMiddleware)
    .route('/user', user)
apps/backend/src/index.ts
// 省略
const app = new Hono<Env>()
	.use(
		'/api/*',
		cors({
			origin: ['https://odyssage.pages.dev', 'http://localhost:5173'],
			allowMethods: ['GET', 'PUT', 'OPTIONS'],
			allowHeaders: ['Content-Type', 'Authorization'],
		}),
	)
    .route('/api', route);

ここでorigin: ['*'],とすると、Cloudflareではstrict-origin-when-cross-originのエラーがでた。
ローカルでは発生しないので注意。

なお、verifyJWTが返すトークンは下記のようなトークンが返ってくる。

token
{
  "provider_id": "anonymous",
  "iss": "https://securetoken.google.com/hogehoge",
  "aud": "hogehoge",
  "auth_time": 1735908952,
  "user_id": "*****************ZZ99",
  "sub": "*****************ZZ99",
  "iat": 1735920914,
  "exp": 1735924514,
  "firebase": { "identities": {}, "sign_in_provider": "anonymous" },
  "uid": "*****************ZZ99"
}

公開したページ

参考

公式

ウェブサイトで Firebase Authentication を使ってみる
JavaScript でカスタム認証システムを使用して Firebase 認証を行う

Firebase Auth の 匿名認証 で爆速オンボーディング!ユーザー離脱を防ぐベストプラクティス

バックエンド

Cloudflare Workers でも Firebase Authentication を使えるぞ!!
Hono, Firebase AuthでToken検証してみる。@hono/firebase-authを使ってみよう!Cloudflare workersで定義する関数に認証チェックをつける
Cloudflare WorkersでFirebase Authのトークン検証を実装する
Firebase Authenticationの認証でCloudflareを利用してFirebase Hostingへのカスタムドメイン登録を省略する
【Cloudflare Pages/D1】個人開発のためのローリスクなクラウドを探し求めてCloudflareに出会う【Next.js】
odyssage - issue

HonoでCORS対応をする

過去にFirebaseを触った記事

react-redux-firebase を導入しようとしたときに起きた@@reactReduxFirebase/LOGINのエラーを消したメモ
Zeit Now + Next.js のページにFirebase認証を導入した際、環境変数にハマったメモ
Laravel6.0 の認証を Firebase のTwitter Oauth認証で行ったメモ
firebase9に更新したら型のエラーがでたので対応したメモ
ワンコイン/月で作るゲームブックプラットフォームの使用技術メモ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?