Skip to content

Firebase Authentication の基本を学ぶ

はじめに

業務で Remix + Firebase Authentication を利用する機会があったので、それらの基本的な使い方について学習した記録です。

環境

バージョン
MacSonoma 14.5
Node.js22.5.1

成果物

https://github.com/kntks/blog-code/tree/main/2024/08/firebase-auth-basic

Firebaseの基礎

プロジェクトの作成

https://console.firebase.google.com/ にアクセスします。

Firebase プロジェクトを使ってみる をクリックします。

firebase-start-page

プロジェクト名を入力して、続行 をクリックします。 firebase-project-name

左のメニューから Authentication をクリックします。 firebase-auth-navi

始めるをクリックします。 firebase-start

Google認証プロバイダの有効化

ログイン方法のタブから、Google を選択します。 firebase-select-provider

有効にする をクリックし、プロジェクトの公開名プロジェクトのサポートメールを入力します。 firebase-activate-google-provider

保存ボタンをクリックし、時間が経つと、Google プロバイダが有効になります。 firebase-activated-google-provider

Webアプリの登録

Firebase の画面左上に歯車のマークがあります。そこからプロジェクトの設定をクリックします。

firebase-project-settings

全般タブの下にスクロールするとアプリケーションを追加する箇所があります。</>をクリックしウェブアプリを追加します。 firebase-project-settings-general-myapp

アプリ名を入力し、アプリを登録をクリックします。 firebase-add-web-app-1

登録完了すると、Firebase SDK の初期化コードが表示されます。このコードは後ほど Remix でコードを書くときに使用します。後で確認できるため、そのまま下にスクロールしコンソールに進むをクリックし、アプリの登録を完了します。 firebase-add-web-app-2

参考:Firebase を JavaScript プロジェクトに追加する - Firebase Documentation

環境変数の設定

今回、Remix で作成したプロジェクトでは、Vite を使用しています。Vite では .env ファイルを使用して環境変数を設定できるため、Firebase の設定情報を .env ファイルに記述します。

.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 のトップページから、以下の特徴が記載されています。

  1. 効率的なデータ取得

    完全に形成されたHTMLドキュメントの送信をすることで高速なロード、スムーズな表示の実現
    サーバー側での並行データ取得

    • 従来の手法:コンポーネント内でのフェッチによるリクエストウォーターフォール
    • Remixの手法:サーバーでのデータ並行ロード
  2. シームレスなサーバーとブラウザランタイム

    • 高速なページロードと瞬時の遷移を実現
    • 分散システムとネイティブブラウザ機能を活用
    • 静的ビルドに頼らない動的な動作
  3. 優れた互換性と柔軟性

    • Web Fetch APIをベースに構築
    • 幅広い環境で動作可能(“it can run anywhere”)
      • Cloudflare Workers
      • サーバーレス環境
      • 従来のNode.js環境

プロジェクトの作成

まずはプロジェクトを作成します。

Terminal window
$ 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

以下のコマンドで開発サーバーを起動できます。

Terminal window
$ pnpm install
$ pnpm dev

Firebase と Remix の統合

firebase モジュールの作成

Terminal window
pnpm install firebase firebase-admin

参考:

初期化と認証関数の実装

initializeApp 関数を使って Firebase を初期化します。

先ほど環境変数の設定.env ファイルを作成し、環境変数を公開しました。フロントエンドでは import.meta.env を使って環境変数を取得します。

app/firebase.ts
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認証の実装

ログインコンポーネントの作成をします。 login-component

signInWithGoogle 関数で認証に成功した後、useSubmit を使ってサーバーに idToken を送信します。

app/routes/login.tsx
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 ライブラリを使用して検証することもできます。

引用:クライアントで ID トークンを取得する - Firebase Documentation

検証には、Firebase Admin SDK を使います。Firebase Admin SDK は、サーバーサイドで Firebase の機能を利用するための SDK です。

参考:ID トークンを検証する - Firebase Documentation

app/firebase.server.ts
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) が行われた場合、アクションはローダーよりも先に呼び出されます。

引用:action - Remix Documentation

もし、エラーになった場合は、redirect 関数を使ってログインページにリダイレクトするようにします。

app/routes/login.tsx
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つ見つけました。

  1. セッション Cookie を利用する
  2. Service Worker を利用する

signInWithPopup を使ってログインすると、idToken が取得できます。しかし、この idToken は有効期限が1時間です。そのため、この記事では、セッション Cookieを作成して、セッションの有効時間を延長します。

5 分から 2 週間までの範囲でカスタムの有効期限を設定してセッション Cookie を作成する機能。

引用:セッション Cookie を管理する - Firebase Documentation

今回は最長の2週間を有効期限として設定します。

app/routes/login.tsx
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.tsxloader を実装して、セッション Cookie を検証します。root に書くことで、すべてのルートでセッションの検証を行います。

app/root.tsx
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 の画面左上に歯車のマークがあります。そこからプロジェクトの設定をクリックします。 firebase-project-settings

全般のタブにウェブAPIキーがあります。 firebase-project-settings-general

参考:Firebase の API キーの使用と管理について学ぶ - Firebase Documentation

アクセストークンがjwtではない?

以下のコードはフロントエンド側で実行されるコードです。そのため console.log で出力すると、Dev Tools のコンソールに表示されます。

signInWithPopup 関数でログインすると、resultUserCredentialImpl クラスのオブジェクトでした。

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._tokenResponseSignInWithIdpResponse 型で、oauthAccessToken が含まれています。

export interface SignInWithIdpResponse extends IdTokenResponse {
oauthAccessToken?: string;
oauthTokenSecret?: string;
nonce?: string;
oauthIdToken?: string;
pendingToken?: 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 のエンドポイントにリクエストを送信します。

Terminal window
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._tokenResponseoauthIdToken だと思われます。(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

参考