【Firebase】特定のページへのアクセス制限の設定方法|管理者のみアクセスできるようにする(カスタムクレイムとは何か?Custom Claims)

firebase-prograshi(プロぐらし)-kv Firebase
記事内に広告が含まれていることがあります。

Firebase Authenticationを使ってアプリを開発していると必ず直面するのが、「特定のユーザーにだけ管理画面を見せたい」という権限管理の壁です。

しかし、Firebaseの標準機能には「管理者フラグ」のような項目は存在しません。

そこで活用すべきなのがカスタムクレイム(Custom Claims)です。これは、ユーザーの身分証(IDトークン)に「admin:true」というデータを付与する機能で、データベースへのアクセスを最小限に抑えつつ、安全なアクセス制限を実現できます。

本記事では、カスタムクレイムの基礎知識から、Next.js(Admin SDK)を用いた具体的な実装手順までを解説しています。

セキュリティを一段上のレベルへ引き上げる準備を始めましょう。


カスタムクレイム(Custom Claims)とは何か?

カスタムクレイムとは、ユーザーのIDトークン(身分証)に独自の項目を書き込める機能のことです。です。

通常、FirebaseのIDトークンには「名前」「メールアドレス」「ユーザーID」といった基本情報しか載っていません。そこに、「この人は管理者(admin)です」という情報を公式に付与します。

Firebaseのサーバー(Admin SDK)を介してのみこの情報を付与することができます


高いセキュリティ

ユーザーが自分のブラウザ上で勝手に「自分は管理者だ」と書き換えることはできない仕組みになっているため、非常に安全です。


通信コストを抑える

また、ユーザーが管理者かどうかを確認するために毎回データベース(Firestoreなど)を読みに行くと、通信の手間(オーバーヘッド)や料金がかかります。

カスタムクレイムを使えば、ユーザーが持っているID自体に admin: true と書いてあるため、データベースを見に行かなくても、その場で即座に権限を確認できるようになります。


権限を設定する大まかな流れ

指定したユーザーに特定の権限(情報)を付与する大まかな流れは次の3STEPです。

  1. 付与(サーバー側): Node.jsなどを使って「このユーザーを管理者にする」という設定を送る。
  2. 反映(クライアント側): ユーザーがログインし直すと、身分証(トークン)の中に設定が含まれる。
  3. 確認(セキュリティルール/コード): 指定したデータがある人だけアクセスを許可する。
権限を付与する処理は1回だけ!

ログイン中の特定のユーザーに権限を付与するためには、ページなどに設定したボタンをクリックしてカスタムクレイムでadmin:trueを付与する関数を実行する必要があります。

実行後は、使用したボタンと権限付与のための関数を削除します。


実装方法

【STEP0】Admin SDKの導入(firebase-admin.tsも)

セキュリティ上、クライアント(ブラウザ)側から自分に管理者権限を付けることはできません。必ずFirebase Admin SDKを使ったサーバーサイドの処理が必要です。

Admin SDKの使い方や初期導入方法については下記をご参考ください。


【STEP1】特定のユーザーに管理者権限を付与するための関数を作成する(firebase-admin.ts)

libディレクトリのfirebase-admin.tsに以下の情報を書き込みます。

import { getAuth } from 'firebase-admin/auth';

//admin カスタマークレイムの付与
export const setAdminRole = async (uid: string) => {
  // 指定したUIDのユーザーに { admin: true } というクレイムを付与
  await getAuth().setCustomUserClaims(uid, { admin: true });
  return { message: "管理者権限を付与しました。再ログイン後に反映されます。" };
};


import * as admin from "firebase-admin";を使っている場合は以下のように記述します。

import * as admin from "firebase-admin";

export const setAdminRole = async (uid: string) => {
  // admin.auth() を通じてアクセスできる
  await admin.auth().setCustomUserClaims(uid, { admin: true });
};


【STEP2】ServerActionsにカスタムクレイムを付与するサーバー関数を作成する(actions.ts)

// app/admin/user-list/actions.ts
"use server";

// setAdminRole をインポートする
import { setAdminRole } from "@/lib/firebase-admin";
import { revalidatePath } from "next/cache";

export async function makeMeAdmin(uid: string) {
  try {
    // lib/firebase-admin.ts で定義した共通関数を呼び出す
    await setAdminRole(uid);
    
    console.log(`User ${uid} is now an admin`);
    revalidatePath("/admin/user-list");
    
    return { success: true, message: "管理者権限を付与しました。再ログインしてください。" };
  } catch (error) {
    console.error(error);
    return { success: false, message: "エラーが発生しました。" };
  }
}


【STEP3】一時的な権限付与用のボタンを作成する(temp/page.tsx)

初回のみログイン中のユーザーに権限を付与するボタンがあるページを作成します。

ここでは、tempというディレクトリの中にpage.tsxを作成します。

"use client";

import { auth } from "@/lib/firebase"; // クライアント用Auth
import { makeMeAdmin } from "@/app/admin/user-list/actions";

export default function AdminSetupPage() {
  const handleAdminUpgrade = async () => {
    // 1. 現在ログインしているユーザーを取得
    const user = auth.currentUser;

    if (!user) {
      alert("ログインしてから実行してください");
      return;
    }

    // 2. Server Actionを実行
    const result = await makeMeAdmin(user.uid);

    if (result.success) {
      alert(result.message);
      alert("実行後はこのボタンを削除してください");
    } else {
      alert("失敗しました: " + result.message);
    }
  };

  return (
    <div className="p-10">
      <h1 className="mb-4">管理者権限付与ツール</h1>
      <button
        onClick={handleAdminUpgrade}
        className="bg-blue-600 text-white px-4 py-2 rounded shadow-md"
      >
        ログイン中の自分を管理者にする
      </button>
    </div>
  );
}
実行後は削除!

このボタンは初回のみ使うものです。一度実行し、ログイン中のユーザーにカスタムクレイムadmin:trueを付与したら、削除します。

※削除しないとこのページにアクセスした人が誰でもボタンを押せてしまします。


【STEP4】ページへのアクセス管理をするコンポーネントを作成する

アクセス管理をして保護したいページ(page.tsx)があるディレクトリにコンポーネントAdminGuard.tsxを作成します。

"use client";

import { useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "@/lib/firebase"; // 上で作った auth をインポート
import { useRouter } from "next/navigation";

export default function AdminGuard({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ログイン状態を監視
    const unsubscribe = onAuthStateChanged(auth, async (user) => {
      try {
        if (!user) {
          router.push("/"); // 未ログインならトップページに飛ばす
          return;
        }

        // カスタムクレイムを確認
        const idTokenResult = await user.getIdTokenResult();
        
        if (idTokenResult.claims.admin) {
          console.log("✅ 管理者確認済み");
          setLoading(false); // 管理者なら表示許可
        } else {
          alert("管理者権限がありません");
          router.push("/"); // 一般ページへ
        }
      } catch (error) {
        console.error("認証チェックエラー:", error);
      }
    });

    return () => unsubscribe();
  }, [router]);

  if (loading) return <p>権限を確認中...</p>;

  return <>{children}</>;
}


これで、管理者としてログインしている場合のみreturn <>{children}</>;を実行します。

{children}は対象のコンポーネント(ここではAdminGuardタグ)で囲んだ中身のタグが入ります。


【STEP5】保護したいページ(内容)をAdminGuardタグで囲む

今回の場合、admin/user-list/page.tsxを管理者にのみ表示させます。

このため、AdminGuardをインポートして、タグで囲みます。

import AdminGuard from "./AdminGuard";

export default async function AdminUserListPage() {
  const users = await getUsers();

  return (
    <AdminGuard>
    元々のタグ
    </AdminGuard>
  );
}


以上でコードの記述は完了です。


【STEP6】ログインしてボタンをクリックする

管理者権限を付与したいアカウントでログインし、先ほど作成した/tempページにアクセスして「ログイン中の自分を管理者にする」ボタンをクリックします。


【STEP7】ボタンとmakeMeAdminを削除する

管理者権限の付与が終わったら、ボタンを使わないので「temp/page.tsx」をディレクトリごと削除します。

また権限付与するためにServerActionsに作成した関数 makeMeAdminも不要なので削除します。


【STEP8】ログアウト・再ログインして対象ページを開く

ログアウト・再ログインして対象ページを開き、正しくアクセスできるか確認します。


また、他のアカウントでログインしてアクセスした場合に、AdminGuard.tsxで指定したようにトップページにリダイレクトするか確認します。


以上で設定は完了です。

お疲れさまでした。

タイトルとURLをコピーしました