Search code examples
reactjsgoogle-cloud-firestorefirebase-authenticationfirebase-securitynext-auth

How to fix FirebaseError: Missing or insufficient permissions in React Hook?


I am using a combination of React Hook, Next.js, and Firebase Auth to create a user coin record in Firestore. This means that as soon as a new user signs up, a new document with a uid should be automatically created for them in Firestore, along with the coins and createdAt fields.

However, when I sign in for the first time, I encounter an error message that says:

hydration-error-info.js:27 Error in createUserCoinRecord: FirebaseError: Missing or insufficient permissions."

However, if I log out and sign in again, the error does not occur anymore.

Upon checking the Firebase database, I noticed that the uid for the user is not created after the first sign-in but is only created after the second sign-in.

Here's the code for the React Hook:

export function useCoinRecord() {
  const { data: session } = useSession();
  const userEmailRef = useRef(null);
  const [created, setCreated] = useState(false);

  async function createUserCoinRecord(uid) {
    await setDoc(doc(db, "users", uid), {
      coins: 100000,
      createdAt: serverTimestamp(),
    });
  }

  useEffect(() => {
    async function createCoinRecordIfNeeded() {
      if (session) {
        if (userEmailRef.current !== session.user.email) {
          userEmailRef.current = session.user.email;
          try {
            await createUserCoinRecord(session.user.email);
            setCreated(true);
          } catch (error) {
            console.error("Error in createUserCoinRecord:", error);
          }
        }
      } else {
        userEmailRef.current = null;
      }
    }

    createCoinRecordIfNeeded();
  }, [session]);

  return { created };
}

And these are my Firestore rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow read, write: if request.auth != null && request.auth.uid == uid;
    }
    match /users/{uid}/{document=**} {
      allow read, write: if request.auth != null && request.auth.uid == uid;
    }
  }
}

The Sign in button:

import { signIn } from "next-auth/react";
//...
      <button onClick={() => signIn("google")}>
        Sign In
      </button>

Sign in with Next Auth:

import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
  ],
};

export default NextAuth(authOptions);

Then use useEffect to sign in with Firebase:

import { useSession } from "next-auth/react";
import { auth, db } from "../firebase/firebase";
//...

  const { data: session } = useSession();
  const { coinBalance, setCoinBalance } = useCoinBalanceContext();
  const [readyToFetchBalance, setReadyToFetchBalance] = useState(false);

  useEffect(() => {
    if (session) {
      signInWithFirebase(session).then((uid) => {
        if (uid) {
          setReadyToFetchBalance(true);
        }
      });
    }
  }, [session]);

  const signInWithFirebase = async (session) => {
    const response = await fetch("/api/firebase/create-custom-token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ user: session.user }),
    });

    const customToken = await response.json();

    return signInWithCustomToken(auth, customToken.token)
      .then((userCredential) => {
        return userCredential.user.uid;
      })
      .catch((error) => {
        console.error("Firebase sign-in error:", error);
      });
  };

  const { created } = useCoinRecord();

  useEffect(() => {
    if (readyToFetchBalance && created && session?.user?.email) {
      (async () => {
        const balance = await getCoinBalance(session.user.email);
        balance && setCoinBalance(balance);
      })();
    }
  }, [session, readyToFetchBalance, created]);

/api/firebase/create-custom-token

import { adminAuth } from "@/firebase/firebaseAdmin";

export default async function handler(req, res) {
  if (req.method !== "POST") {
    res.status(405).json({ message: "Method not allowed" });
    return;
  }

  const { user } = req.body;

  const uid = user.email;

  try {
    const customToken = await adminAuth.createCustomToken(uid);

    res.status(200).json({ token: customToken });
  } catch (error) {
    console.error("Error creating custom token:", error);
    res.status(500).json({ message: "Error creating custom token" });
  }
}

firebase.js

import { getApp, getApps, initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
apiKey: process.env.FIREBASE_APIKEY,
authDomain: process.env.FIREBASE_AUTHDOMAIN,
databaseURL: process.env.FIREBASE_DATABASEURL,
projectId: process.env.PROJECTID,
storageBucket: process.env.FIREBASE_STORAGEBUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGINGSENDERID,
appId: process.env.FIREBASE_APPID,
measurementId: process.env.FIREBASE_MEASUREMENTID,
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
export { db, auth };

firebaseAdmin.js

import admin from "firebase-admin";

const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY);
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });
}

const adminDb = admin.firestore();
const adminAuth = admin.auth();

export { adminDb, adminAuth };

My database is structured like this:

/users/[email protected]/coins

where [email protected] is the uid.

What could be causing the FirebaseError and how can I fix it please?


Solution

  • I realized that there is a race between two hooks that are running at the same time.

    The issue is that useCoinRecord() tries to create a new document before the user is authenticated. To fix this, I removed useCoinRecord() and modified the useEffect() hook to include the necessary logic for creating the user coin record before setting the ReadyToFetchBalance flag to true.

    The updated code looks like this:

    useEffect(() => {
      if (session) {
        signInWithFirebase(session).then(async (uid) => {
          if (uid) {
            await createUserCoinRecord(uid);
            setReadyToFetchBalance(true);
          }
        });
      }
    }, [session]);
    

    This should solve the race condition problem and ensure that the document is only created after the user is authenticated.