【Firebase】Authのユーザー情報をFirestoreにも保存する方法を実例で解説(Next.js)

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

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(認証)が提供する、ユーザーがログインしているか、ログアウトしたかの変化をリアルタイムで監視するリスナー(監視役)です。

以下の変化を検知して、指定した処理を実行します。

onAuthStateChangedが検知する処理
  1. ユーザーがログインした
  2. ユーザーがログアウトした
  3. ページをリロードした(ログイン状態の再確認)
const unsubscribe = onAuthStateChanged(auth, async (user) => {検知時に実行する処理})


注意点

onAuthStateChangedは初回ログイン時だけでなく、リロードやログアウト時などログイン状態が変わる度に実行されるため、リソースを無駄に消費します。

初期ログイン時のデータ保存は、Cloud FunctionsのonCreateを使う方が実用的です


カスタムクレーム

Authに保存するデータに独自に情報を付け加えることができます。この情報をカスタムクレームといいます。

例えば、管理者となるアカウントには admin: true といったデータを付与します。

以下でauthに保存されたユーザーデータの中のカスタムクレームを取得しています。

const idTokenResult = await user.getIdTokenResult();
const isAdmin = !!idTokenResult.claims.admin;
カスタムクレームとは何か?


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);                  
 
    }
  }
}
.getを使う理由

「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にも保存することが推奨です

カスタムクレームをFirestoreにも保存することが推奨な理由
  1. Authのユーザー情報は条件検索が苦手なため
    Firestoreに同期することで、誰が管理者権限を持っているか?などの検索が容易になります。
  2. Firestoreの情報はあくまで参照用のデータとし、認証はAuthで行う
    データはFirestoreに置き、実際の認証にはカスタムクレームを使う方法が最も安全です。
  3. セキュリティルールで対策する
    セキュリティルールで、users コレクションの roleやadminフィールドを本人でも変更できないように制限します。


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