Firebase Authを使えば簡単に認証機能を実装できますが、「ユーザーのプロフィール画像を表示したい」「ユーザー一覧を管理したい」と考えたとき、Authの情報だけでは不十分なことに気づきます。
Authには最小限のデータしか保持できないため、詳細な属性を扱うにはFirestoreとの連携が不可欠です。
本記事では、Next.jsを用いてFirebase Authのユーザー情報をFirestoreへ自動的に保存・同期する方法を実例付きで解説しています。
さらに、セキュリティ的に懸念されやすい「権限(admin)情報の安全な管理方法」や、セキュリティルールでの保護についても触れています。
【手順1】FirestoreとAuthenticationのアプリを作成する
Next.jsを使ってCloud FirestoreとFirebase Authenticationを実装したアプリを作成します。
以下のようなアプリが作成できます。

【手順2】コンポーネントの作成:AuthProvider.tsx
components/AuthProvider.tsxを作成し、その中に以下のコードを記述します。
mkdir components
ni components/AuthProvider.tsx"use client";
import { useEffect } from "react";
import { auth, db } from "@/lib/firebase";
import { onAuthStateChanged } from "firebase/auth";
import { doc, setDoc } from "firebase/firestore";
export default function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
//ログイン状態を監視
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
const idTokenResult = await user.getIdTokenResult();
const isAdmin = !!idTokenResult.claims.admin;
const userData = {
uid: user.uid,
email: user.email,
displayName: user.displayName || "名無しユーザー",
admin: isAdmin,
updatedAt: new Date(),
};
//setDocの{ merge: true }で上書き保存
const userRef = doc(db, "users", user.uid);
await setDoc(userRef, userData, { merge: true });
console.log("Firestore synced");
}
});
return () => unsubscribe();
}, []);
return <>{children}</>;
}ログイン状態を監視し、ログイン状態に変更があった場合に、そのユーザーのデータとカスタムクレーム(admin)を取得します。
取得したデータを userDataに格納し、Firestoreのusersコレクションに上書き保存する処理です。
なぜカスタムフックではなく、コンポーネントに記述するのか?
今回使用する処理は、ログイン時に1回のみ動けばいいものです。再利用性が低いためhooksディレクトリの中に記述する必要はありません。
また、カスタムフックを使う場合は、呼び出した先もクライアントコンポーネント(use client)でないといけないというNext.jsの制約があります。
カスタムフックにした場合、layout.tsxに記述するとlayout.tsxにuse clientをつけなければいけなくなります。
サーバーコンポーネントでサーバー側でHTMLを作成し爆速でページ表示できるのがNext.jsのメリットなのに、全てのページで読み込まれるlayout.tsxをクライアントコンポーネントにしてしまうとその機能がなくなってしまいます。
これを防ぐために、今回の処理はコンポーネントとして作成します。
onAuthStateChanged・・・ユーザーのログイン状態の監視
onAuthStateChangedは、Firebase Authentication(認証)が提供する、ユーザーがログインしているか、ログアウトしたかの変化をリアルタイムで監視するリスナー(監視役)です。
以下の変化を検知して、指定した処理を実行します。
- ユーザーがログインした
- ユーザーがログアウトした
- ページをリロードした(ログイン状態の再確認)
const unsubscribe = onAuthStateChanged(auth, async (user) => {検知時に実行する処理})onAuthStateChangedは初回ログイン時だけでなく、リロードやログアウト時などログイン状態が変わる度に実行されるため、リソースを無駄に消費します。
初期ログイン時のデータ保存は、Cloud FunctionsのonCreateを使う方が実用的です。
カスタムクレーム
Authに保存するデータに独自に情報を付け加えることができます。この情報をカスタムクレームといいます。
例えば、管理者となるアカウントには admin: true といったデータを付与します。
以下でauthに保存されたユーザーデータの中のカスタムクレームを取得しています。
const idTokenResult = await user.getIdTokenResult();
const isAdmin = !!idTokenResult.claims.admin;カスタムクレームの詳細については下記をご参考ください。
> 【Firebase】特定のページへのアクセス制限の設定方法|管理者のみアクセスできるようにする(カスタムクレームとは何か?Custom Claims)
doc・・・ドキュメントデータの取得
doc(db, "users", user.uid);docはデータベースとコレクション名、ユーザーIDで、指定したドキュメントのデータを取得する関数です。
ここでは、usersコレクションの指定したユーザーIDのデータを取得しています。
setDoc・・・ドキュメントのデータ更新
setDoc(userRef, userData, { merge: true });setDocは指定したドキュメントのデータを、指定したデータで保存する関数です。
オプションで{ merge: true }とすることで、データを新規保存ではなく上書きします。
【手順3】layout.tsxでカスタムフックを呼び出す
作成したコンポーネントを layout.tsx など、アプリ全体で読み込まれるファイルで呼び出します。
これにより、Googleログインでもパスワードログインでも、ログイン完了の瞬間にFirestoreが更新されます。
既存のlayout.tsxに以下を追記します。
冒頭でAuthProviderを呼び出す。
import AuthProvider from "@/components/AuthProvider";
{children}をAuthProvderコンポーネントで囲む
<AuthProvider>
{children}
</AuthProvider>
全体像は以下のような記述になります。
// layout.tsx (サーバーコンポーネントのまま)
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import AuthProvider from "@/components/AuthProvider"; // 追加
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{/* AuthProvider で children を囲む */}
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}【手順4】セキュリティルールの追加
Firestoreに保存した後は、他人が勝手に他人のユーザー情報を書き換えないよう、firestore.rules を設定します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//usersコレクションは自分のデータのみ編集可能
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
// adminフィールドを含めずに作成するか、含めるなら必ず false であることを強制
allow create: if request.auth.uid == userId
&& request.resource.data.get('admin', false) == false;
//adminフィールドの変更を禁止する。adminフィールドが存在しない場合はfalseにする
allow update: if request.auth.uid == userId
&& request.resource.data.get('admin', false) == resource.data.get('admin', false);
}
}
}「request.resource.data.admin」と「request.resource.data.get(‘admin’, false)」はgetをつけるかつけないかの違いです。
getをつけずにdata.adminを指定すると、adminフィールドがない場合にエラーになります。
data.get(‘admin’, 存在しない場合の値)として場合は、adminフィールドがなければ第二引数をセット適用します。
上記ファイルを保存したら対象のファイルのみデプロイします。
firebase deploy --only firestore:rules※セキュリティルールのデプロイにはFirebase CLIが必要です。詳細は下記をご参考ください。
【手順5】新規登録/ログインしてみる
トップページから新規登録または既存アカウントでログインすると、指定した情報が全てFirestoreに保存されていることがわかります。

(補足)カスタムクレームのadmin情報をFirestoreに保存してもいいか?
カスタムクレームでadmin:trueといった管理者情報を付与して、その情報をFirestoreに保存すると、データの閲覧や編集・削除などが悪用されるのではないか?と考える人もいるかもしれません。
結論から言うと、Firestoreにも保存することが推奨です。
- Authのユーザー情報は条件検索が苦手なため
Firestoreに同期することで、誰が管理者権限を持っているか?などの検索が容易になります。 - Firestoreの情報はあくまで参照用のデータとし、認証はAuthで行う
データはFirestoreに置き、実際の認証にはカスタムクレームを使う方法が最も安全です。 - セキュリティルールで対策する
セキュリティルールで、users コレクションの roleやadminフィールドを本人でも変更できないように制限します。


