概要
CloudflareでホスティングしているサイトでFirebase Authenticationを使って認証を行う。今回は匿名認証を試す。
フロー
Firebaseのプロジェクト作成
Firebaseは利用できるようになっている前提とする。*
Firebase Consoleから「プロジェクトを作成する」を選択。
フロントエンド
環境変数は.env
ファイルの内容をコピペすると簡単に入力できる。すごい。
プレビューで確認したければ、環境を切り替えて同様に環境変数を設定する必要がある。
バックエンドとの通信のために、バックエンドのドメインを環境変数VITE_BACKEND_DOMAIN
に追加する。
プレビューとプロダクションで値は異なるはず。これに間違えてフロントエンドのドメインを指定したところ、405 Method Not Allowed
のエラーが発生した。指定間違いに注意。
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;
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();
};
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);
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設定
バックエンド
wrangler secret put FIREBASE_PROJECT_ID
で環境変数にfirebaseのProjectIdを追加。
Hono FirebaseMiddlewareのメンテ頻度が少ないため、firebase-auth-cloudflare-workersを利用した。
firebase-adminだと、Uncaught TypeError: globalThis.XMLHttpRequest is not a constructor
のエラーが出てしまうらしい。*
# 省略
[observability]
enabled = true
+ [vars]
+ PUBLIC_JWK_CACHE_KEY = "odyssage-public-jwk-cache"
+ [[kv_namespaces]]
+ binding = "PUBLIC_JWK_CACHE_KV"
+ id = "<先ほど設定したKVのid>"
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
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);
};
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();
});
// 省略
const route = new Hono<Env>()
.get('/', (c) => c.text('Hello Cloudflare Workers!'))
.use('/user/*', authorizeMiddleware)
.route('/user', user)
// 省略
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が返すトークンは下記のようなトークンが返ってくる。
{
"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
過去にFirebaseを触った記事
react-redux-firebase を導入しようとしたときに起きた@@reactReduxFirebase/LOGINのエラーを消したメモ
Zeit Now + Next.js のページにFirebase認証を導入した際、環境変数にハマったメモ
Laravel6.0 の認証を Firebase のTwitter Oauth認証で行ったメモ
firebase9に更新したら型のエラーがでたので対応したメモ
ワンコイン/月で作るゲームブックプラットフォームの使用技術メモ