DEV Community

Cover image for Firebase Auth Duplicate Email Error: How to Fix It Step-by-Step
naymur
naymur

Posted on

Firebase Auth Duplicate Email Error: How to Fix It Step-by-Step

While building a Next.js app with Firebase Authentication (email/password), I encountered a frustrating issue — users could sign up multiple times with the same email address, creating duplicate entries in my Firestore database.

Even though Firebase Auth is supposed to prevent duplicate emails automatically, I was still seeing duplicates. After digging through GitHub issues and Reddit discussions, I realized this problem has been around for a while.

After spending an entire day, I finally managed to resolve the issue in a tricky way. All you have to do is use Firestore Database for this.


My Original (Problematic) Code

const createUserProfile = async (user: any) => {
  try {
    // **** // Problem: Using UID as document ID
    const userDocRef = doc(db, "users", user.uid);

    const userProfile = {
      uid: user.uid,
      email: user.email,
      userType: "staff",
      createdAt: serverTimestamp(),
      updatedAt: serverTimestamp(),
    };

    await setDoc(userDocRef, userProfile);
    console.log("User profile created");
  } catch (error) {
    console.error("Error creating user profile:", error);
    throw error;
  }
};

const handleSignUp = async (e: React.FormEvent) => {
  e.preventDefault();
  setLoading(true);
  setError("");

  try {
    // ***** // No check before creating account
    const userCredential = await createUserWithEmailAndPassword(
      auth,
      email,
      password
    );

    await createUserProfile(userCredential.user);

    router.push("/dashboard");
  } catch (error: any) {
    if (error.code === "auth/email-already-in-use") {
      setError("Email already registered.");
    }
  } finally {
    setLoading(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

Why This Approach Failed

The problem with this common approach:

  1. No pre-check: We only found out the email was taken AFTER trying to create the Firebase Auth account
  2. **UID **as document ID: We used user.uid as the Firestore document ID, but we don't know the UID until AFTER the user is created
  3. Can’t check by email: With UID as the document ID, we couldn’t easily check if an email already exists in Firestore
  4. fetchSignInMethodsForEmail() doesn't work: This Firebase method is often disabled due to "Email Enumeration Protection" for security reason s

The Solution: Use Email as Document ID

The key insight: Use the email address as the Firestore document ID instead of the UID. This way, we can check if an email exists BEFORE attempting to create the Firebase Auth account.

const createUserProfile = async (user: any) => {
    try {
      //****// Use EMAIL as document ID instead of UID
      const userDocRef = doc(db, "users", user.email);
      const userDoc = await getDoc(userDocRef);

      if (!userDoc.exists()) {
        const userProfile = {
          uid: user.uid, 
          email: user.email,
          userType: "staff",
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        };

        await setDoc(userDocRef, userProfile);
      } else {
        console.log("User profile already exists");
      }
    } catch (error) {
      console.error("Error creating user profile:", error);
      throw error;
    }
  };

 const handleSignUp = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError("");

    try {

      //****// Check if email exists in Firestore BEFORE creating auth account
      const userDocRef = doc(db, "users", email);
      const userDoc = await getDoc(userDocRef);

      if (userDoc.exists()) {
        setError("You already have an account with this email. Please sign in instead.");
        setLoading(false);
        return; 
      }

      //****// Create Firebase Auth account
      const userCredential = await createUserWithEmailAndPassword(
        auth,
        email,
        password
      );


      //****// Create Firestore profile
      await createUserProfile(userCredential.user);
      router.push("/dashboard");

    } catch (error: any) {
      if (error.code === "auth/email-already-in-use") {
        setError("You already have an account with this email. Please sign in instead.");
      } else if (error.code === "auth/weak-password") {
        setError("Password should be at least 6 characters.");
      } else if (error.code === "auth/invalid-email") {
        setError("Invalid email address.");
      } else {
        setError(error.message);
      }
    } finally {
      setLoading(false);
    }
  };
Enter fullscreen mode Exit fullscreen mode

**

Key Changes Explained

**
1. Changed Document ID from UID to Email
Before:

//***// Can't check email existence before signup
const userDocRef = doc(db, "users", user.uid);
Enter fullscreen mode Exit fullscreen mode

After:

//***// Can check email existence anytime
const userDocRef = doc(db, "users", user.email);
Enter fullscreen mode Exit fullscreen mode

2. Added Pre-Check Before Creating Account
Before:

//***// No check - try to create and handle error later
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
Enter fullscreen mode Exit fullscreen mode

After:

//***// Check Firestore first
const userDocRef = doc(db, "users", email);
const userDoc = await getDoc(userDocRef);

if (userDoc.exists()) {
  setError("You already have an account with this email. Please sign in instead.");
  return; 
}

//***// Only create account if email doesn't exist
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
Enter fullscreen mode Exit fullscreen mode

3. Store UID as a Field

const userProfile = {
  uid: user.uid, // ✅ Still keep UID for reference
  email: user.email,
  userType: "staff",
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp(),
};

Enter fullscreen mode Exit fullscreen mode

** ## Why This Solution Works **

✅ Benefits:

  1. Fast email lookup: Direct document access instead of querying
// Super fast O(1) lookup
const userDoc = await getDoc(doc(db, "users", email));
Enter fullscreen mode Exit fullscreen mode
  1. Prevents duplicates: Checks BEFORE attempting Firebase Auth signup
  2. Better UX: Users get immediate feedback without waiting for Firebase Auth error
  3. No enumeration protection issues: Doesn’t rely on fetchSignInMethodsForEmail()
  4. Easy queries: Finding users by email is now trivial
//****// Get user by email
   const user = await getDoc(doc(db, "users", "user@example.com"));
Enter fullscreen mode Exit fullscreen mode

Still have UID: Stored as a field for any UID-based operations

Firestore Structure

Your Firestore users collection now looks like this:

users/
  ├── user@example.com/
  │   ├── uid: "abc123xyz"
  │   ├── email: "user@example.com"
  │   ├── userType: "staff"
  │   ├── createdAt: timestamp
  │   └── updatedAt: timestamp
  │
  └── another@example.com/
      ├── uid: "def456uvw"
      ├── email: "another@example.com"
      └── ...

Enter fullscreen mode Exit fullscreen mode

**

What About Existing Users?

**

If you already have users with UID as document ID, you have a few options:

//***// One-time migration script
const migrateUsers = async () => {
  const usersRef = collection(db, "users");
  const snapshot = await getDocs(usersRef);

  for (const docSnap of snapshot.docs) {
    const userData = docSnap.data();

    //***// Create new document with email as ID
    await setDoc(doc(db, "users", userData.email), userData);

    //***// Optional: Delete old document
    //***// await deleteDoc(doc(db, "users", docSnap.id));
  }
};
Enter fullscreen mode Exit fullscreen mode

NOTE: If you’re in development, delete old users and use the new structure

Here’s the Whole Sign-up Code:

"use client";
import React, { useState } from "react";
import { createUserWithEmailAndPassword } from "firebase/auth";
import { doc, setDoc, getDoc, serverTimestamp } from "firebase/firestore";
import { auth, db } from "@/firebase/client";
import { useRouter } from "next/navigation";
import { Input } from "../ui/input";
import Link from "next/link";

export default function SignUpForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const createUserProfile = async (user: any) => {
    try {
      //***// Use EMAIL as document ID instead of UID
      const userDocRef = doc(db, "users", user.email);
      const userDoc = await getDoc(userDocRef);

      if (!userDoc.exists()) {
        const userProfile = {
          uid: user.uid, // Store UID as a field for reference
          email: user.email,
          userType: "staff",
          createdAt: serverTimestamp(),
          updatedAt: serverTimestamp(),
        };

        await setDoc(userDocRef, userProfile);
        console.log("User profile created successfully");
      } else {
        console.log("User profile already exists");
      }
    } catch (error) {
      console.error("Error creating user profile:", error);
      throw error;
    }
  };

  const handleSignUp = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError("");

    try {

      //***// Check if email exists in Firestore BEFORE creating auth account
      const userDocRef = doc(db, "users", email);
      const userDoc = await getDoc(userDocRef);

      if (userDoc.exists()) {
        setError("You already have an account with this email. Please sign in instead.");
        setLoading(false);
        return; 
      }

      //***// Create Firebase Auth account
      const userCredential = await createUserWithEmailAndPassword(
        auth,
        email,
        password
      );

      //***// Create Firestore profile
      await createUserProfile(userCredential.user);

      router.push("/dashboard");

    } catch (error: any) {
      if (error.code === "auth/email-already-in-use") {
        setError("You already have an account with this email. Please sign in instead.");
      } else if (error.code === "auth/weak-password") {
        setError("Password should be at least 6 characters.");
      } else if (error.code === "auth/invalid-email") {
        setError("Invalid email address.");
      } else {
        setError(error.message);
      }
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-neutral-900 py-12 px-4">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-medium capitalize text-gray-100">
            Create your account
          </h2>
          <p className="mt-2 text-center text-4xl text-gray-400">
            Welcome to Inspire
          </p>
        </div>

        <div className="mt-8 space-y-6">
          {error && (
            <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
              {error}
            </div>
          )}

          <div className="space-y-4">
            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium text-gray-400"
              >
                Email address
              </label>
              <Input
                id="email"
                name="email"
                type="email"
                required
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                className="mt-1 appearance-none relative block w-full h-12 border"
                placeholder="Email address"
              />
            </div>

            <div>
              <label
                htmlFor="password"
                className="block text-sm font-medium text-gray-400"
              >
                Password
              </label>
              <Input
                id="password"
                name="password"
                type="password"
                required
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                className="mt-1 appearance-none relative block w-full h-12 border"
                placeholder="Password (min 6 characters)"
              />
            </div>
          </div>

          <div>
            <button
              type="button"
              onClick={handleSignUp}
              disabled={loading}
              className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
            >
              {loading ? "Creating account..." : "Sign Up"}
            </button>
          </div>

          <div className="text-center">
            <Link
              href="/signin"
              className="text-indigo-600 hover:text-indigo-500 text-sm"
            >
              Already have an account? Sign in
            </Link>
          </div>
        </div>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)