Firebase Authentication の基本を学ぶ
はじめに
業務で 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 ✔ Template copied
git Initialize a new git repository? No
deps Install dependencies with npm? No ◼ Skipping install step. Remember to install dependencies after setup with npm install.
done That's it! Check out README.md for development and deploy instructions.
Join the community at https://rmx.as/discord
以下のコマンドで開発サーバーを起動できます。
$ pnpm install$ pnpm dev
Firebase と Remix の統合
firebase モジュールの作成
pnpm install firebase firebase-admin
参考:
初期化と認証関数の実装
initializeApp 関数を使って Firebase を初期化します。
先ほど環境変数の設定で.env ファイルを作成し、環境変数を公開しました。フロントエンドでは import.meta.env
を使って環境変数を取得します。
const firebaseConfig = { 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> => { try { const result = await signInWithPopup(auth, new GoogleAuthProvider()); return await getIdToken(result.user) } catch (error) { console.error("Google Sign-In Error:", error); return null; }};
export { signInWithGoogle };
Remix内でのFirebase認証の実装
ログインコンポーネントの作成をします。
signInWithGoogle
関数で認証に成功した後、useSubmit を使ってサーバーに idToken
を送信します。
export default function Login() { const submit = useSubmit(); const handleLogin = async () => { const credential = await signInWithGoogle(); if (!credential) { console.error("Login failed"); return }
submit({"idToken": credential || null}, {method: "post"}); }
return ( <div> <h1>ログイン</h1> <button 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" onClick={handleLogin} >Googleでログイン </button> </div> );}
ID トークン取得後に検証を行う
IdToken
をサーバーに送信した後、Firebase Admin SDK を使って IdToken
を検証します。
ID トークンを取得した後、該当する JWT をバックエンドに送信し、Firebase Admin SDK を使用して、その JWT を検証できます。Firebase でネイティブにサポートされていない言語でサーバーが記述されている場合は、サードパーティの JWT ライブラリを使用して検証することもできます。
検証には、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);
export { adminAuth };
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) が行われた場合、アクションはローダーよりも先に呼び出されます。
もし、エラーになった場合は、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"); }
// idTokenを検証 await adminAuth.verifyIdToken(idToken).catch(() => redirect("/login"));}
セッション管理
セッション管理方法はドキュメントを読む限り2つ見つけました。
signInWithPopup
を使ってログインすると、idToken
が取得できます。しかし、この idToken
は有効期限が1時間です。そのため、この記事では、セッション Cookieを作成して、セッションの有効時間を延長します。
5 分から 2 週間までの範囲でカスタムの有効期限を設定してセッション Cookie を作成する機能。
今回は最長の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"); }
// idTokenを検証 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"); }
try { const decodedToken = await adminAuth.verifySessionCookie(token) return json({ name: decodedToken.name }) } catch (error) {
// 検証に失敗した場合はログイン画面にリダイレクト if(error instanceof FirebaseAuthError) { console.error(error.code, error.message); throw redirect("/login"); } throw error; }}
参考:セッション Cookie を確認して権限をチェックする - Firebase Documentation
その他
ここでは、学習の過程で学んだことを記録しています。
API key の確認方法
Firebase の画面左上に歯車のマークがあります。そこからプロジェクトの設定
をクリックします。
全般
のタブにウェブAPIキー
があります。
参考:Firebase の API キーの使用と管理について学ぶ - Firebase Documentation
アクセストークンがjwtではない?
以下のコードはフロントエンド側で実行されるコードです。そのため console.log
で出力すると、Dev Tools のコンソールに表示されます。
signInWithPopup
関数でログインすると、result
は UserCredentialImpl
クラスのオブジェクトでした。
const signInWithGoogle = async () => { try { const result = await signInWithPopup(auth, new GoogleAuthProvider());
console.log(result) const idToken = await getIdToken(result.user) // const credential = GoogleAuthProvider.credentialFromResult(result) return idToken } catch (error) { console.error("Google Sign-In Error:", error); return null; }};
そして UserCredentialImpl._tokenResponse
は SignInWithIdpResponse
型で、oauthAccessToken
が含まれています。
export interface SignInWithIdpResponse extends IdTokenResponse { oauthAccessToken?: string; oauthTokenSecret?: string; nonce?: string; oauthIdToken?: string; pendingToken?: string;}
その 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", "exp": "1722909912", "expires_in": "3466", "email": "foo.bar@gmail.com", "email_verified": "true", "access_type": "online"}
引用:アクセス トークン
ちなみに 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", "email_verified": true, "at_hash": "hdWT6DA_4VIKnSG7_TrO3Q", "iat": 1722906313, "exp": 1722909913}
refreshToken で idToken を更新するには?
リフレッシュトークンをIDトークンに交換する - Firebase Documentation