【Firebase Authentication】保存してあるユーザー情報の一覧ページと削除・編集機能を作成する方法|Firebase Admin SDKとは何か?JSONの中身や秘密鍵の使い方を実例で解説

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

Firebase Authenticationでユーザー管理画面を作ろうとして、「クライアントSDKだけではユーザー一覧が取得できない」という壁に当たっていませんか?

この問題を解決するのが、強力な権限を持つ「Firebase Admin SDK」です。

本記事では、Admin SDKの基礎知識から、導入に不可欠なサービスアカウント(JSON)の取得・秘密鍵の安全な扱い方までを解説しています。

実際のコード例を交え、ユーザー一覧の表示、そして運用に欠かせない編集・削除機能の実装したアプリを構築しています。


全ユーザー情報を取得する方法

Authenticationに登録されている全てのユーザー情報を取得する方法には以下の2つがあります。

全ユーザー情報を取得する方法
  1. Firebase Admin SDKを使用する
  2. Firestoreにユーザーデータを同期する


Firebase Admin SDKとは?利用時のメリデメ

Firebase Admin SDKとは何か?

Firebase Admin SDKは、特権(管理者権限)を持ってFirebaseを操作するための開発キットです。

パスワードを知らなくてもユーザーのメールアドレスを変更する、特定のユーザーを無効化したり、強制的に削除したりする、セキュリティルールを無視して、全データの読み書きやバックアップを行うといったことができます。

Admin SDKはいつ使う?

Admin SDKの使用が向いているケースは「全ユーザーに一括でメールを送りたい」「管理者専用パネルでユーザーを強制削除したい」など、運営側のメンテナンス用途です。


Firebase Admin SDKの注意点

Admin SDKは非常に強力な権限を持つため、悪意のあるユーザーに中身を見られる可能性がある環境(ブラウザやスマホアプリ内)では絶対に動かしてはいけません

次のような場合のみ使用するようにします。

Firebase Admin SDKの使用環境
  1. Cloud Functions for Firebase(最も一般的。Firebase上のサーバーレス環境)
  2. 自社サーバー(オンプレミス、AWS、GCP上のNode.js/Pythonサーバーなど)
  3. 自分のPC(ローカル環境で一括データ処理スクリプトを実行する時など)


メリット

今回のようなAuthenticationのユーザー一覧を取得する場合、次のようなメリットがあります。

  1. 確実で安全
    Auth に登録されている「本当のユーザー数」を正確に取得できます。
  2. 実装が楽
    listUsers() という関数を呼び出すだけで一覧が手に入ります。


デメリット

  1. サーバーが必要
    セキュリティ上の理由から、ブラウザやスマホアプリから直接実行することはできません。 Cloud Functions などのサーバー環境が必要です。
  2. 検索が弱い
    「名前のあいまい検索」や「フォロワー数順に並べる」といった複雑なクエリは一切できません。
  3. 表示が遅い
    数万人以上のユーザーがいる場合、ページネーション(分割読み込み)の処理が重くなります。


Firestoreとは?利用時のメリデメ

Firestoreとは?

Firestore(Cloud Firestore)は、NoSQLのクラウド型データベースです。

SQL形式ではなく、「ドキュメント(JSONのようなデータ)」を「コレクション(フォルダのようなもの)」に入れて管理します。構造が自由なので、データの追加や変更が簡単です。


Authenticationとの同期方法

Authenticationを使ってユーザーが新規登録した瞬間に、ユーザーの情報をFirestoreのusers コレクションなどに保存します

MEMO

Cloud Functionsを使えば、AuthenticationとFirestoreを自動で同期することができます。Cloud Functionsは内部的にはFirebase Admin SDK を使用しています。


メリット

今回のようなAuthenticationのユーザー一覧を取得する場合、次のようなメリットがあります。

  1. 自由な検索・フィルタリング
    「東京住みのユーザーだけ表示」「ログイン順に並べる」といった、アプリに必要な表示が簡単にできます。
  2. 追加データを保持できる
    趣味、自己紹介、ポイント残高など、Auth 側には持てない独自のプロフィール情報を一緒に管理できます。
  3. クライアントから直接呼べる
    セキュリティルールを適切に設定すれば、アプリから直接 users コレクションを読み取って一覧表示できます。


Admin SDKでユーザー一覧を作成する方法

今回は、管理者用のユーザー一覧管理ページを作成します。

なお、Authenticationを使ったユーザーログインを行うアプリの作成方法については下記をご参考ください。


ここでは上記の続きとして、既にAuthenticationを使ってユーザー情報が保存できる/されている状態から始めます。


秘密鍵(JSON)の取得

Admin SDKは、通常のFirebase SDK(フロントエンド用)とは異なり、サーバー側でフルアクセス権限を持つため、秘密鍵の生成が必要です。

Firebaseコンソールの対象のアプリに入り、「プロジェクトの概要 > プロジェクトの設定」へと進みます。


「サービスアカウント」タグをクリックし、「新しい秘密鍵を生成」をクリックします。


「キーを生成」をクリックします。


JSONファイルがダウンロードされます。


JSONファイルの中身・・・使用する3つのデータ

JSONファイルには以下のデータが入っています。

{
  "type": "service_account",
  "project_id": "・・・",
  "private_key_id": "・・・",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE・・・"
  "client_email": "firebase-adminsdk-fbsvciam・・・.gserviceaccount.com",
  "client_id": "・・・",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/cert",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc・・・",
  "universe_domain": "googleapis.com"
}

このプロパティのうち、Admin SDKで使うのは以下の3つです。

Admin SDKで使う3つのデータ
  1. project_id
  2. client_email
  3. private_key

多くのライブラリ(特に firebase-admin SDK)は、最低限 「どこに(Project ID)」「誰が(Client Email)」「どの鍵で(Private Key)」 アクセスするかさえ分かれば、残りの項目をデフォルト値で補完するように設計されています。

「private_key_id」と「client_id」は主に監査や詳細なログ、特定の高度な認証フローで使われますが、通常のデータ操作(FirestoreやAuthの管理)では省略しても通ることが多いです。

その他のプロパティはGoogleの共通認証エンドポイントなので、SDKが内部で「Googleの標準ならここだろう」と自動補完します。

Firebase 秘密鍵プロパティ一覧表

プロパティ名用途必須どういうときに必要になるか
project_idプロジェクトを識別するID必須常に必要。 どのプロジェクトのリソース(Firestore等)を操作するかを特定するため。
private_key認証用の署名に使う秘密鍵必須常に必要。 これがないと「本人確認」ができず、アクセスが拒否されます。
client_emailサービスアカウントのアドレス必須常に必要。 Google側で「どの権限を持つユーザーか」を判別するために使用します。
typeアカウントの種類(service_account)任意SDKが「これはサービスアカウントだな」と自動認識するために使用します。
private_key_id鍵の識別用ID任意複数の鍵をローテーション(更新)している場合などに、どの鍵での署名かを識別するのに使われます。
client_idサービスアカウント固有の数値ID任意低レベルのOAuth2ライブラリを使用する場合や、特定のGoogle APIを直接叩く場合に必要になることがあります。
auth_uri認可用のURL任意通常はSDKが内部でデフォルト値(Google公式URL)を使うため、指定しなくても動きます。
token_uriトークン取得用のURL任意同上。Googleの認証サーバーの場所を示すものです。
auth_provider_…公開鍵証明書のURL任意取得したトークンの妥当性を検証する際に参照されます。通常は自動補完されます。
universe_domainクラウドのドメイン任意Google Cloudの特殊な環境(政府専用クラウドなど)以外、通常(https://www.google.com/search?q=googleapis.com)は不要です。


.env.localに秘密鍵を登録する

.env.localに上記3つの情報を保存します。Next.jsで環境変数を登録する際は「NEXT_PUBLIC_環境変数名_大文字」とします。

FIREBASE_PROJECT_ID="プロジェクトID(JSON内のproject_id)"
FIREBASE_CLIENT_EMAIL="メールアドレス形式のID(JSON内のclient_email)"
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
注意点

NEXT_PUBLICをつけてはいけません。

NEXT_PUBLIC_ あり: ブラウザ(クライアント側)に公開されます。
NEXT_PUBLIC_ なし: サーバーサイドでのみ参照され、ブラウザには隠されます。

※project_idはブラウザに渡しても問題ない情報ですが、Admin SDKの利用において、ブラウザに渡す必要はありません(ただし、.env.localでNEXT_PUBLICをつけているのであれば合わせる必要があります)

なぜ.env.localを使うのか?

なぜ、firebase-admin.tsにこれらの情報を直接記述せずに、.env.localに記述してその内容を読み込むのかについては、下記をご参考ください。

> 【Next.js】firebase.jsやNEXT_PUBLIC_, process.env.とは何か?.env.localの環境変数を読み込む方法

> .envと.env.localとは何か?どちらを使うべき?違い・環境変数の優先順位や使い方を実例で分かりやすく解説


lib\firebase-admin.tsで環境変数を読み込む

libディレクトリの中にfirebase-admin.tsを作成します。Admin SDK を初期化して使うためのファイルです。

.env.localの環境変数をfirebase-admin.tsで読み込みます。

import * as admin from "firebase-admin";

if (!admin.apps.length) {
  try {
    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        // 環境変数からの読み込み時、改行文字 (\n) が正しく解釈されるように replace を挟む
        privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
      }),
    });
  } catch (error) {
    console.error("Firebase Admin 初期化エラー:", error);
  }
}

export const adminAuth = admin.auth();
注意点

絶対に秘密鍵を直接記載しないでください。外部に漏洩してはいけない重要な情報です。

なぜFirebaseコンソールのコードを使わないのか?

FirebaseコンソールのAdmin SDKのに、参考のコードが載っています。ですが、ここで使用したコードはそれとは異なるものです。

今回使用したコードと最終的にやっていることは同じで、Admin SDKの初期化時に必要な情報を渡しています。

以下は、秘密鍵などの情報をJSONファイルで読み込んでいます。シンプルですが、.gitignoreを正しく記載しないと間違って秘密鍵の載ったJSONファイルをアップロードしてしまうリスクがあります。

秘密鍵などの重要情報は.env.localに記載し、別途読み込む方がより安全でセキュリティが向上します。

▼Firebaseコンソールのコード(構成スニペット)


Admin SDKのインストール

プロジェクトのルートディレクトリで以下のコードを実行します。

npm install firebase-admin

firebase-admin というパッケージをNode.js プロジェクトに追加(node_modules にインストール)し、package.json に依存関係として記録します。

firebase-admin=Firebase Admin SDKと捉えて問題ありません。

MEMO

Firebase Admin SDKが商品名、firebase-adminがnpmで使うためのパッケージ名です。



ServerActionsの作成(app/ファイルパス/actions.ts)

Admin SDKでAuthenticationのデータを取得する方法には「Server Actions」と「API Routes」の2つがあります。

今回は、現在主流となっている「Server Actions」を使います。

Server Actions(サーバーアクション)は、Next.js(App Router)で使えるサーバー上で直接実行される関数です。API Route(/api/...)を作らなくても、フォーム送信やボタンクリックからサーバー処理を安全に呼び出すことができます

ServerActionsとは何か?


今回作成するユーザー一覧は管理者のみが使う用途なので、admin/user-listというURLにし、ファイルパスもそれに合わせます。

"use server";

import { adminAuth } from "@/lib/firebase-admin";
import { revalidatePath } from "next/cache";

// ユーザー一覧取得
export async function getUsers() {
  const listUsers = await adminAuth.listUsers();
  return listUsers.users.map((user) => ({
    uid: user.uid,
    email: user.email,
    displayName: user.displayName || "未設定",
    disabled: user.disabled,
    provider: user.providerData[0]?.providerId,
  }));
}

// ユーザーの無効化・有効化
export async function toggleUserStatus(uid: string, currentStatus: boolean) {
  try {
    await adminAuth.updateUser(uid, { disabled: !currentStatus });
    revalidatePath("/admin/user-list");
  } catch (error) {
    console.error("Status update error:", error);
  }
}

// ユーザー削除
export async function deleteUser(uid: string) {
  try {
    await adminAuth.deleteUser(uid);
    revalidatePath("/admin/user-list");
  } catch (error) {
    console.error("Delete user error:", error);
  }
}

// ユーザー情報(displayName)の編集
export async function updateDisplayName(uid: string, formData: FormData) {
  const newName = formData.get("displayName") as string;
  try {
    await adminAuth.updateUser(uid, { displayName: newName });
    revalidatePath("/admin/user-list");
  } catch (error) {
    console.error("Update name error:", error);
  }
}

//更新
export async function refresh() {
  revalidatePath("/admin/user-list");
}


管理画面の作成(app/ファイルパス/page.tsx)

ServerActionsで取得したAuthenticationのユーザー一覧情報を管理画面に表示します。

その際「表示名(displayName)」「状態(一時停止/有効)」「削除」ができるようにします。

// app/admin/user-list/page.tsx
import { getUsers, toggleUserStatus, deleteUser, updateDisplayName, refresh } from "./actions";
import RefreshButton from "./RefreshButton";

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

  return (
    <div className="p-8 bg-gray-50 min-h-screen">
      <h1 className="text-2xl font-bold mb-6 text-gray-800">ユーザー管理</h1>

      {/* 更新ボタン */}
      <div className="my-[28px]">
        <form action={refresh}>
          <RefreshButton />
        </form>
      </div>
      
      <div className="overflow-hidden bg-white rounded-xl shadow-sm border border-gray-200">
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">ユーザー情報 / 名前変更</th>
              <th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">状態</th>
              <th className="px-6 py-4 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">操作</th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {users.map((user) => (
              <tr key={user.uid} className={user.disabled ? "bg-gray-50" : ""}>
                {/* ユーザー情報 & 名前編集フォーム */}
                <td className="px-6 py-4">
                  <div className="text-sm text-gray-500 mb-2">{user.email}</div>
                  <form action={updateDisplayName.bind(null, user.uid)} className="flex items-center gap-2">
                    <input
                      name="displayName"
                      defaultValue={user.displayName}
                      className="border border-gray-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 outline-none text-gray-600"
                    />
                    <button type="submit" className="text-xs bg-blue-50 text-blue-600 hover:bg-blue-100 px-2 py-1 rounded transition">
                      更新
                    </button>
                  </form>
                </td>

                {/* 状態ラベル */}
                <td className="px-6 py-4">
                  {user.disabled ? (
                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
                      停止中
                    </span>
                  ) : (
                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
                      アクティブ
                    </span>
                  )}
                </td>

                {/* 操作ボタン群 */}
                <td className="px-6 py-4 text-right space-x-3">
                  {/* 一時停止/有効化ボタン:色の出し分け */}
                  <form action={async () => { "use server"; await toggleUserStatus(user.uid, user.disabled); }} className="inline">
                    <button 
                      className={`text-sm font-medium px-4 py-1.5 rounded-lg transition ${
                        user.disabled 
                        ? "bg-gray-400 text-white"
                        : "bg-yellow-500 hover:bg-yellow-600 text-white shadow-sm" // 有効なときはイエロー
                      }`}
                    >
                      {user.disabled ? "有効化する" : "一時停止する"}
                    </button>
                  </form>
                  
                  {/* 削除ボタン */}
                  <form action={async () => { "use server"; await deleteUser(user.uid); }} className="inline">
                    <button 
                      className="text-sm font-medium text-red-600 hover:text-red-800 px-2 py-1 transition"
                      // onClickを使いたい場合はここをClient Componentにする必要があります
                    >
                      削除
                    </button>
                  </form>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}


npm run devでアプリを起動し「http://localhost:3000/admin/user-list」を叩きます。

注意点

Firebase Authenticationと通信し状態の変更を行いますが、リアルタイム監視はしていません。このため、Firebaseコンソールでステータスを変更しても自動で内容が切り替わりません。

最新の状態を反映するにはリロードする必要があります。(更新ボタンで最新情報を同期します)



コード解説:ServerActions(app/ファイルパス/actions.ts)

use server

"use server";

Server Actions(サーバーアクション)を使うための宣言です。

今回のように、秘密鍵をブラウザには渡さずサーバー内でのみAdmin SDKと接続するために必須の宣言です。

use serverとは何か?


全ユーザーの取得:adminAuth.listUsers()

import { adminAuth } from "@/lib/firebase-admin";

// ユーザー一覧取得
export async function getUsers() {
  const listUsers = await adminAuth.listUsers();
  return listUsers.users.map((user) => ({
    uid: user.uid,
    email: user.email,
    displayName: user.displayName || "未設定",
    disabled: user.disabled,
    provider: user.providerData[0]?.providerId,
  }));
}

まずは冒頭で、Admin SDKのauth機能を「adminAuth」として読み込みます。


const listUsers = await adminAuth.listUsers();

listUsers関数は、Authenticationに存在するユーザー一覧を配列で取得します。※最大 1000件まで

ユーザーが1000件以上ある場合

listUsers関数は、一度に最大1000人までしか返しませんが、レスポンスの中に「次の1000人はここから始まりますよ」という印(pageToken)を含めてくれます。これを利用して、ループ処理で順番に取得します。

※ユーザー数が多い場合、サーバーのメモリを使い果たしてクラッシュするリスクや実行時間の制限にひっかかるリスクがあります。

「特定のユーザーを探したい」という目的であれば、listUsersで全件回すのは非効率です。Authenticationのユーザー情報は複雑な検索(名前の部分一致など)もできません。

このため、検索が必要な場合は、ユーザー作成時にFirestoreにもユーザー情報を保存しておき、Firestore側でクエリ(検索)を行うのが一般的な設計です。(Cloud FunctionsでAdmin SDKとFirestoreを接続できます)


mapを使って、取得したデータのユーザー情報から必要なものだけを抜き出します。

return listUsers.users.map((user) => ({・・・})

Authenticationのユーザー情報には以下が登録されています。

    uid: user.uid, //Firebase が発行するユーザー固有ID
    email: user.email, //ログイン用メールアドレス(未設定の場合は undefined)
    displayName: user.displayName || "未設定", //表示名
    disabled: user.disabled, //trueの場合、ログイン不可
    provider: user.providerData[0]?.providerId, //ログイン方法(password, google.com, github.comなど)

なお、サーバーから取得した各ユーザーのデータは以下のようになっています。

[
  {
    uid: "abc123",
    email: "test@example.com",
    displayName: "山田太郎",
    disabled: false,
    provider: "google.com"
  },
  ...
]


データ更新後の画面即時反映:revalidatePath(“パス”)

import { revalidatePath } from "next/cache";
revalidatePath("/admin/user-list");

revalidatePathは、Next.js のキャッシュを無効化(破棄)して再取得するための関数です。

データ更新後に画面を即時反映させるために使います


ユーザーの無効/有効の切り替え:adminAuth.updateUser(uid, 変更したい項目のプロパティ名: 値);

await adminAuth.updateUser(uid, { disabled: !currentStatus });

adminAuth.updateUser(uid, 変更したい項目のプロパティ名: 値);は、指定したuidのユーザーのプロパティを切り替える関数です。

ここでは、disabledの値を切り替えています。


ユーザーの削除:adminAuth.deleteUser(uid)

adminAuth.deleteUser(uid)

指定したuidのユーザーを削除します。


表示名の更新:adminAuth.updateUser(uid, { displayName: newName });

// ユーザー情報(displayName)の編集
export async function updateDisplayName(uid: string, formData: FormData) {
  const newName = formData.get("displayName") as string;
  try {
    await adminAuth.updateUser(uid, { displayName: newName });
    revalidatePath("/admin/user-list");
  } catch (error) {
    console.error("Update name error:", error);
  }
}

FormDataはHTML共通のオブジェクトで、フォームの入力内容が入っています。今回の場合、page.tsxのinputタグと連動しています。

                  <form action={updateDisplayName.bind(null, user.uid)} className="flex items-center gap-2">
                    <input
                      name="displayName"
                      defaultValue={user.displayName}
                      className="border border-gray-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 outline-none text-gray-600"
                    />
                    <button type="submit" className="text-xs bg-blue-50 text-blue-600 hover:bg-blue-100 px-2 py-1 rounded transition">
                      更新
                    </button>
                  </form>

name=”displayName”の値を文字列として取得し、newNameという定数に代入します。

const newName = formData.get("displayName") as string;


受け取った値を指定したuidのユーザー名として更新します。

await adminAuth.updateUser(uid, { displayName: newName });

更新が終わったら、ページのキャッシュをクリアし、再ロードして最新情報を画面に表示しています。



注意点:誰でもアクセスできてしまう

今回作成したAuthenticationのユーザー一覧ページはURLを知っている人なら誰でもアクセスできてしまいます。

本番環境で使う場合は、指定したアカウントでしかアクセスできないといったセキュリティ対策が必要です。

Firebaseにはサーバー側でアクセスを管理する「セキュリティルール」という便利で強力な機能があります。

セキュリティルールの詳細は下記をご参考ください。

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