You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firebase signInWithPopup未返回auth/account-exists-with-different-credential问题

问题描述

我开发了一个同时支持邮箱密码登录与Google登录的页面,遇到异常情况:当用户先通过邮箱+密码完成注册,再使用Google登录时,signInWithPopup会直接移除该账号的邮箱密码登录方式,且全程未触发Firebase官方文档中提及的任何错误。我预期会收到auth/account-exists-with-different-credential错误,但实际并未返回。

单点登录按钮实现代码

import { btnVariants } from "./CustomButton";
import { app } from "../firebase/client";
import { getAuth, signInWithPopup, GoogleAuthProvider, TwitterAuthProvider } from 'firebase/auth';
import { getFirestore, doc, getDoc } from "firebase/firestore";
import { setInitialUserProfile } from "@/utils/helpers/setInitialUserProfile";

export function SingleSignIn({ redirectPath, type }) {
    const auth = getAuth(app);
    const db = getFirestore(app);

    const methods = [
        {
            label: "Google",
            icon: "/images/icon-google.png",
            id: "google.com"
        },
        {
            label: "Twitter",
            icon: "/images/icon-twitter.png",
            id: "twitter.com"
        },
    ];

    const handleSignIn = async (providerId) => {
        let provider;

        // 获取对应登录提供商
        if (providerId === 'google.com') {
            provider = new GoogleAuthProvider();
        } else if (providerId === 'twitter.com') {
            provider = new TwitterAuthProvider();
        }

        provider.addScope('email');

        try {
            const userCredential = await signInWithPopup(auth, provider)
            .then(function (result) {
                var user = result.user;
                var credential = result.credential;

                console.log('✅success', user, credential)
            }, function (error) {
                console.log('❌ failed', error)
                var email = error.email;
                var credential = error.credential;
                console.log('❌ failed', email, credential)
                if (error.code === 'auth/account-exists-with-different-credential') {
                    auth.fetchSignInMethodsForEmail(email).then(function (providers) {
                        console.log('❌ failed, already exists', providers)
                    });
                }
            })


            } catch (error) {
                console.error(`Error signing in with ${providerId}:`, error);
            }
        }

        return (
            <div className="flex flex-col">

                <div className={`flex flex-col gap-2 w-full`}>
                    {methods.map((method, n) => (
                        <div key={n} className="flex w-full flex-col gap-1 items-center">
                            <button
                                onClick={() => handleSignIn(method.id)}
                                className={`${btnVariants({ variant: "outline" })} w-full items-center justify-center`}
                                type="button"
                                disabled={method.disabled}
                            >
                                <div className="gap-2 flex items-center justify-center">
                                    <img src={method.icon} alt="" className="w-4 h-4 mr-2" />
                                    <span className="font-sans capitalize font-medium">
                                        Continue with {method.label}
                                    </span>
                                </div>
                            </button>
                        </div>
                    ))}
                </div>

                <div className="relative my-6 md:my-8 xl:my-10">
                    <div className="absolute inset-0 flex items-center">
                        <span className="w-full border-t border-neutral-200 dark:border-neutral-700"></span>
                    </div>
                    <div className="relative flex justify-center text-xs">
                        <span className="bg-background px-2 lg:px-4 font-sans font-medium text-foreground">Or</span>
                    </div>
                </div>

            </div>
        )
    }



            //     const idToken = await userCredential.user.getIdToken()

            //     // 检查用户Profile是否已创建
            //     const userProfileRef = doc(
            //         db,
            //         "userProfiles",
            //         userCredential.user.uid,
            //     );
            //     const userProfile = await getDoc(userProfileRef);

            //     // 如果不存在则创建初始Profile
            //     if (!userProfile.data()) {
            //         await setInitialUserProfile(userCredential);
            //     }

            //     // 登录到后端
            //     try {
            //         const response = await fetch("/api/auth/signin", {
            //             method: "GET",
            //             headers: {
            //                 Authorization: `Bearer ${idToken}`,
            //             },
            //         });

            //         const res = await response.json()
            //         console.log('response', res)

            //         console.log('redirectPath', redirectPath)
            //         // 页面跳转
            //         if (redirectPath) window.location.assign(redirectPath);
            //         window.location.assign("/dashboard");
            //     } catch (error) {
            //         console.log("❌ 登录错误");
            //     }
问题分析与解决方案

核心问题

代码中同时混用了async/await和.then()/.catch()的错误处理方式,导致auth/account-exists-with-different-credential错误被.then()的回调吃掉,外层try/catch无法捕获,最终错误处理逻辑完全失效。Firebase在这种情况下会默认执行账号合并,直接移除原有的密码登录方式。

修复后的代码实现

统一使用async/await+try/catch处理流程,确保所有错误都能被正确捕获和处理:

import { btnVariants } from "./CustomButton";
import { app } from "../firebase/client";
import { getAuth, signInWithPopup, GoogleAuthProvider, TwitterAuthProvider, fetchSignInMethodsForEmail } from 'firebase/auth';
import { getFirestore, doc, getDoc } from "firebase/firestore";
import { setInitialUserProfile } from "@/utils/helpers/setInitialUserProfile";

export function SingleSignIn({ redirectPath, type }) {
    const auth = getAuth(app);
    const db = getFirestore(app);

    const methods = [
        {
            label: "Google",
            icon: "/images/icon-google.png",
            id: "google.com"
        },
        {
            label: "Twitter",
            icon: "/images/icon-twitter.png",
            id: "twitter.com"
        },
    ];

    const handleSignIn = async (providerId) => {
        let provider;

        // 获取对应登录提供商
        if (providerId === 'google.com') {
            provider = new GoogleAuthProvider();
        } else if (providerId === 'twitter.com') {
            provider = new TwitterAuthProvider();
        }

        provider.addScope('email');

        try {
            const result = await signInWithPopup(auth, provider);
            const user = result.user;
            const credential = result.credential;
            console.log('✅登录成功', user, credential);

            // 获取用户ID Token
            const idToken = await user.getIdToken();
            
            // 检查用户Profile是否已创建
            const userProfileRef = doc(db, "userProfiles", user.uid);
            const userProfile = await getDoc(userProfileRef);
            
            // 如果不存在则创建初始Profile
            if (!userProfile.data()) {
                await setInitialUserProfile(result);
            }
            
            // 登录到后端
            const response = await fetch("/api/auth/signin", {
                method: "GET",
                headers: {
                    Authorization: `Bearer ${idToken}`,
                },
            });
            
            const res = await response.json();
            console.log('后端响应', res);
            
            // 页面跳转
            if (redirectPath) {
                window.location.assign(redirectPath);
            } else {
                window.location.assign("/dashboard");
            }
        } catch (error) {
            console.error(`使用${providerId}登录失败:`, error);
            
            // 处理账号已存在的错误
            if (error.code === 'auth/account-exists-with-different-credential') {
                const email = error.email;
                const pendingCredential = error.credential;
                
                // 获取该邮箱已关联的登录方式
                const providers = await fetchSignInMethodsForEmail(auth, email);
                console.log('❌ 账号已存在,关联的登录方式:', providers);
                
                // 示例逻辑:如果关联了密码登录,提示用户输入密码后合并账号
                if (providers.includes('password')) {
                    // 这里可以替换为自定义的密码输入弹窗
                    // const password = prompt('请输入该邮箱的登录密码,完成账号关联');
                    // if (password) {
                    //     const passwordCred = await signInWithEmailAndPassword(auth, email, password);
                    //     await linkWithCredential(passwordCred.user, pendingCredential);
                    //     console.log('✅ 账号关联成功');
                    // }
                }
            }
        }
    }

    return (
        <div className="flex flex-col">
            <div className={`flex flex-col gap-2 w-full`}>
                {methods.map((method, n) => (
                    <div key={n} className="flex w-full flex-col gap-1 items-center">
                        <button
                            onClick={() => handleSignIn(method.id)}
                            className={`${btnVariants({ variant: "outline" })} w-full items-center justify-center`}
                            type="button"
                            disabled={method.disabled}
                        >
                            <div className="gap-2 flex items-center justify-center">
                                <img src={method.icon} alt="" className="w-4 h-4 mr-2" />
                                <span className="font-sans capitalize font-medium">
                                    继续使用 {method.label}
                                </span>
                            </div>
                        </button>
                    </div>
                ))}
            </div>

            <div className="relative my-6 md:my-8 xl:my-10">
                <div className="absolute inset-0 flex items-center">
                    <span className="w-full border-t border-neutral-200 dark:border-neutral-700"></span>
                </div>
                <div className="relative flex justify-center text-xs">
                    <span className="bg-background px-2 lg:px-4 font-sans font-medium text-foreground">或</span>
                </div>
            </div>
        </div>
    )
}

关键修改说明

  1. 统一错误处理:移除.then()/.catch()回调,全部使用async/await+try/catch,确保所有Firebase错误都能被捕获
  2. 恢复完整流程:将原本注释掉的用户Profile创建、后端登录逻辑移到try块内,保证登录流程完整
  3. 正确处理账号冲突:在catch块中针对auth/account-exists-with-different-credential错误添加了明确处理逻辑,可根据业务需求实现账号关联或提示用户切换登录方式

额外注意事项

  • 确保Firebase控制台的Authentication模块中,邮箱/密码和Google登录方式均已启用
  • 检查是否开启了"自动合并账号"功能(默认关闭,若开启可能导致错误不触发)
  • 测试时需保证Google账号邮箱与之前注册的邮箱完全一致

内容的提问来源于stack exchange,提问作者Jay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 06:57:03