はじめに
業務で Remix + Firebase Authentication を利用する機会があったので、それらの基本的な使い方について学習した記録です。
環境
バージョン Mac Sonoma 14.5 Node.js 22.5.1
成果物
https://github.com/kntks/blog-code/tree/main/2024/08/firebase-auth-basic
Firebaseの基礎
プロジェクトの作成
https://console.firebase.google.com/ にアクセスします。
Firebase プロジェクトを使ってみる
をクリックします。
プロジェクト名を入力して、続行
をクリックします。
左のメニューから Authentication
をクリックします。
始める
をクリックします。
Google認証プロバイダの有効化
ログイン方法
のタブから、Google
を選択します。
有効にする
をクリックし、プロジェクトの公開名
、プロジェクトのサポートメール
を入力します。
保存
ボタンをクリックし、時間が経つと、Google プロバイダが有効になります。
Webアプリの登録
Firebase の画面左上に歯車のマークがあります。そこからプロジェクトの設定
をクリックします。
全般
タブの下にスクロールするとアプリケーションを追加する箇所があります。</>
をクリックしウェブアプリを追加します。
アプリ名を入力し、アプリを登録
をクリックします。
登録完了すると、Firebase SDK の初期化コードが表示されます。このコードは後ほど Remix でコードを書くときに使用します。後で確認できるため、そのまま下にスクロールしコンソールに進む
をクリックし、アプリの登録を完了します。
参考:Firebase を JavaScript プロジェクトに追加する - Firebase Documentation
環境変数の設定
今回、Remix で作成したプロジェクトでは、Vite を使用しています。Vite では .env
ファイルを使用して環境変数を設定できるため、Firebase の設定情報を .env
ファイルに記述します。
VITE_FIREBASE_API_KEY="Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VITE_FIREBASE_AUTH_DOMAIN="my-firebase-test-xyz.firebaseapp.com"
VITE_FIREBASE_PROJECT_ID="my-firebase-test-xyz"
VITE_FIREBASE_STORAGE_BUCKET="my-firebase-test-xyz.appspot.com"
VITE_FIREBASE_MESSAGING_SENDER_ID="11111111111"
VITE_FIREBASE_APP_ID="1:11111111111:web:bbbbbbbbbbbbbbbb"
VITE_FIREBASE_MEASUREMENT_ID="G-"
参考:
Remix の基礎
Remix フレームワークの特徴
Remix のトップページ から、以下の特徴が記載されています。
効率的なデータ取得
完全に形成されたHTMLドキュメントの送信をすることで高速なロード、スムーズな表示の実現
サーバー側での並行データ取得
従来の手法:コンポーネント内でのフェッチによるリクエストウォーターフォール
Remixの手法:サーバーでのデータ並行ロード
シームレスなサーバーとブラウザランタイム
高速なページロードと瞬時の遷移を実現
分散システムとネイティブブラウザ機能を活用
静的ビルドに頼らない動的な動作
優れた互換性と柔軟性
Web Fetch APIをベースに構築
幅広い環境で動作可能(“it can run anywhere”)
Cloudflare Workers
サーバーレス環境
従来のNode.js環境
プロジェクトの作成
まずはプロジェクトを作成します。
$ npx create-remix@latest
remix v2.10.3 💿 Let's build a better website...
dir Where should we create your new project?
◼ Using basic template See https://remix.run/guides/templates for more
git Initialize a new git repository?
deps Install dependencies with npm?
◼ Skipping install step. Remember to install dependencies after setup with npm install.
Check out README.md for development and deploy instructions.
Join the community at https://rmx.as/discord
以下のコマンドで開発サーバーを起動できます。
Firebase と Remix の統合
firebase モジュールの作成
pnpm install firebase firebase-admin
参考:
初期化と認証関数の実装
initializeApp 関数を使って Firebase を初期化します。
先ほど環境変数の設定 で.env ファイル を作成し、環境変数を公開しました。フロントエンドでは import.meta.env
を使って環境変数を取得します。
apiKey: import . meta . env . VITE_FIREBASE_API_KEY ,
authDomain: import . meta . env . VITE_FIREBASE_AUTH_DOMAIN ,
projectId: import . meta . env . VITE_FIREBASE_PROJECT_ID ,
storageBucket: import . meta . env . VITE_FIREBASE_STORAGE_BUCKET ,
messagingSenderId: import . meta . env . VITE_FIREBASE_MESSAGING_SENDER_ID ,
appId: import . meta . env . VITE_FIREBASE_APP_ID ,
measurementId: import . meta . env . VITE_FIREBASE_MEASUREMENT_ID
} as const satisfies FirebaseOptions ;
const authApp = getApps (). length ? getApp () : initializeApp ( firebaseConfig );
const auth = getAuth ( authApp );
const signInWithGoogle = async () : Promise < string | null > => {
const result = await signInWithPopup ( auth , new GoogleAuthProvider ());
return await getIdToken ( result . user )
console . error ( "Google Sign-In Error:" , error );
export { signInWithGoogle };
Remix内でのFirebase認証の実装
ログインコンポーネントの作成をします。
signInWithGoogle
関数で認証に成功した後、useSubmit を使ってサーバーに idToken
を送信します。
export default function Login () {
const submit = useSubmit ();
const handleLogin = async () => {
const credential = await signInWithGoogle ();
console . error ( "Login failed" );
submit ({ "idToken" : credential || null }, { method: "post" });
className = "text-white bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#4285F4]/55 me-2 mb-2"
ID トークン取得後に検証を行う
IdToken
をサーバーに送信した後、Firebase Admin SDK を使って IdToken
を検証します。
ID トークンを取得した後、該当する JWT をバックエンドに送信し、Firebase Admin SDK を使用して、その JWT を検証できます。Firebase でネイティブにサポートされていない言語でサーバーが記述されている場合は、サードパーティの JWT ライブラリを使用して検証することもできます。
引用:クライアントで ID トークンを取得する - Firebase Documentation
検証には、Firebase Admin SDK を使います。Firebase Admin SDK は、サーバーサイドで Firebase の機能を利用するための SDK です。
参考:ID トークンを検証する - Firebase Documentation
import { AppOptions , initializeApp , applicationDefault , getApps , getApp } from "firebase-admin/app" ;
import { getAuth as getAdminAuth } from "firebase-admin/auth" ;
const firebaseAdminConfig = {
credential: applicationDefault (),
projectId: import . meta . env . VITE_FIREBASE_PROJECT_ID
} as const satisfies AppOptions ;
const app = getApps (). length === 0 ? initializeApp ( firebaseAdminConfig ) : getApp ();
const adminAuth = getAdminAuth ( app );
Remix の action はサーバー側で実行される関数であるため、この関数内で Firebase Admin SDK を使い IdToken
を検証します。
A route action is a server only function to handle data mutations and other actions. If a non-GET request is made to your route (DELETE, PATCH, POST, or PUT) then the action is called before the loaders.
ルートアクションは、データの変異やその他のアクションを処理するためのサーバのみの関数です。 ルートに対して GET 以外のリクエスト (DELETE、PATCH、POST、PUT) が行われた場合、アクションはローダーよりも先に呼び出されます。
引用:action - Remix Documentation
もし、エラーになった場合は、redirect
関数を使ってログインページにリダイレクトするようにします。
export const action = async ({ request } : ActionFunctionArgs ) => {
const idToken = await request . formData (). then ( d => d . get ( "idToken" ));
if ( ! idToken || typeof idToken !== "string" ) {
return redirect ( "/login" );
await adminAuth . verifyIdToken ( idToken ). catch (() => redirect ( "/login" ));
セッション管理
セッション管理方法はドキュメントを読む限り2つ見つけました。
セッション Cookie を利用する
Service Worker を利用する
signInWithPopup
を使ってログインすると、idToken
が取得できます。しかし、この idToken
は有効期限が1時間です。そのため、この記事では、セッション Cookieを作成して、セッションの有効時間を延長します。
5 分から 2 週間までの範囲でカスタムの有効期限を設定してセッション Cookie を作成する機能。
引用:セッション Cookie を管理する - Firebase Documentation
Firebase の ID トークンは有効期限が1時間
今回は最長の2週間を有効期限として設定します。
export const action = async ({ request } : ActionFunctionArgs ) => {
const idToken = await request . formData (). then ( d => d . get ( "idToken" ));
if ( ! idToken || typeof idToken !== "string" ) {
return redirect ( "/login" );
await adminAuth . verifyIdToken ( idToken ). catch (() => redirect ( "/login" ));
const sess = await adminAuth . createSessionCookie ( idToken , { expiresIn: 1000 * 60 * 60 * 24 * 14 });
const session = await getSession ( request . headers . get ( "Cookie" ));
session . set ( "token" , sess );
return redirect ( "/home" , { headers: { "Set-Cookie" : await commitSession ( session )} });
セッション Cookieの検証
root.tsx
に loader を実装して、セッション Cookie を検証します。root に書くことで、すべてのルートでセッションの検証を行います。
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const url = new URL ( request . url )
if ( url . pathname === "/login" ) return {}
const sessionCookie = await getSession ( request . headers . get ( "Cookie" ));
if ( ! sessionCookie . has ( "token" )) return redirect ( "/login" )
const token = sessionCookie . get ( "token" )
if ( ! token || typeof token !== "string" ) {
return redirect ( "/login" );
const decodedToken = await adminAuth . verifySessionCookie ( token )
return json ({ name: decodedToken . name })
// 検証に失敗した場合はログイン画面にリダイレクト
if ( error instanceof FirebaseAuthError ) {
console . error ( error . code , error . message );
throw redirect ( "/login" );
参考:セッション Cookie を確認して権限をチェックする - Firebase Documentation
その他
ここでは、学習の過程で学んだことを記録しています。
API key の確認方法
Firebase の画面左上に歯車のマークがあります。そこからプロジェクトの設定
をクリックします。
全般
のタブにウェブAPIキー
があります。
参考:Firebase の API キーの使用と管理について学ぶ - Firebase Documentation
アクセストークンがjwtではない?
以下のコードはフロントエンド側で実行されるコードです。そのため console.log
で出力すると、Dev Tools のコンソールに表示されます。
signInWithPopup
関数でログインすると、result
は UserCredentialImpl
クラスのオブジェクトでした。
const signInWithGoogle = async () => {
const result = await signInWithPopup ( auth , new GoogleAuthProvider ());
const idToken = await getIdToken ( result . user )
// const credential = GoogleAuthProvider.credentialFromResult(result)
console . error ( "Google Sign-In Error:" , error );
そして UserCredentialImpl._tokenResponse
は SignInWithIdpResponse
型で、oauthAccessToken
が含まれています。
export interface SignInWithIdpResponse extends IdTokenResponse {
oauthAccessToken ?: string ;
oauthTokenSecret ?: string ;
https://github.com/firebase/firebase-js-sdk/blob/firebase%4010.12.4/packages/auth/src/api/authentication/idp.ts#L42-L48
その oauthAccessToken
は “ya29.a0AcM612zW590ciWWN234VM6J5V9ddferdrgasdfae-cJpd2HNz79z8ZOvFwH_JkOhasdfasefbksasSEsetPmKtcJouCUMLILekdhfuJCNLehfes8r0Fe3c3mU8_aaaaaaaaaa9DXSlTC5KxqkRcvRdlhVbpXAaCgYKwervasdfgarfasdzid_7Q132AsJYQUZ5FQxxxxx” のような文字列でした。
この oauthAccessToken
は JWT ではなく、不透明なトークン
らしいです。
アクセス トークンは、OAuth 2.0 フレームワークに準拠する不透明トークンです
引用:アクセス トークン
アクセストークンの情報を取得するためには、以下のように oauthAccessToken
を使って、Google のエンドポイントにリクエストを送信します。
curl "https://oauth2.googleapis.com/tokeninfo?access_token=ya29.a0AcM612zW590ciWWN234VM6J5V9ddferdrgasdfae-cJpd2HNz79z8ZOvFwH_JkOhasdfasefbksasSEsetPmKtcJouCUMLILekdhfuJCNLehfes8r0Fe3c3mU8_aaaaaaaaaa9DXSlTC5KxqkRcvRdlhVbpXAaCgYKwervasdfgarfasdzid_7Q132AsJYQUZ5FQxxxxx"
"azp" : "333333333333-xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"aud" : "333333333333-xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"sub" : "123456789123456789",
"scope" : "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid",
"email" : "foo.bar@gmail.com",
"email_verified" : "true",
引用:アクセス トークン
ちなみに OIDC の仕様における id_token
は、firebase の場合、おそらく UserCredentialImpl._tokenResponse
の oauthIdToken
だと思われます。(at_hash が入っている)実際に JWT をデコードし、Payload を確認すると、以下のような情報が含まれていました。
"iss" : "accounts.google.com" ,
"azp" : "333333333333-xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com" ,
"aud" : "333333333333-xxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com" ,
"sub" : "123456789123456789" ,
"email" : "foo.bar@gmail.com" ,
"at_hash" : "hdWT6DA_4VIKnSG7_TrO3Q" ,
refreshToken で idToken を更新するには?
リフレッシュトークンをIDトークンに交換する - Firebase Documentation
参考