解决Firebase NodeJS SDK注册登录报错auth/error-code:-47问题
问题背景
我开发了一个注册表单,允许用户输入邮箱、密码等凭证,目标是创建Firebase Auth账户,并在MongoDB中存储其他用户信息。流程设定为:用户提交表单后,先发送POST请求将信息保存到MongoDB,再调用getAuth()提供的createUserWithEmailAndPassword方法创建账户;同时在Firebase Functions中使用firebase-functions/v2/identity的beforeUserCreated和beforeUserSignedIn方法修改账户声明。
近期持续遇到错误,日志信息有限,且signInWithGoogle也出现相同浏览器错误:
FirebaseError: Firebase: Error (auth/error-code:-47)
核心代码
1. 邮箱注册处理函数emailSignUp
function emailSignUp(event) { setSignUpClicked(true); event.preventDefault(); if (password !== confirmPassword) { console.log("Passwords do not match!"); setSignUpClicked(false); return; } let formData = new FormData(document.getElementById("emailSignUp")); fetch(`${process.env.REACT_APP_DEPLOYED_API_URL}/auth/user/register`, { method: "POST", body: formData, }) .then((response) => response.json()) .then((data) => { if (data.status === "error") { setStatus("failure"); setSignUpClicked(false); throw new Error("Invalid form input. Please check again."); } else { createUserWithEmailAndPassword(auth, email, password) .then((userCredential) => { setStatus("success"); setSignUpClicked(false); setShowMyModal(true); }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; setStatus("failure"); setSignUpClicked(false); console.log(error); }); console.log("Success:", data); } }) .catch((error) => { setStatus("failure"); setSignUpClicked(false); console.error("Error:", error); }); }
2. Firebase Functions的index.js
export const beforecreated = beforeUserCreated(async (event) => { try { const email = event.data.email; const checkUser = await User.findOne({ email: event.data.email }); if (event.additionalUserInfo.providerId === "google.com") { const checkOrg = await Organization.findOne({ email }); if (checkUser) { return { displayName: checkUser.name, customClaims: { role: "user", userId: checkUser._id, }, }; } else if (checkOrg) { return { displayName: checkUser.name, customClaims: { role: "org", orgId: checkUser._id, }, }; } else { const name = event.data.displayName; const email = event.data.email; const photoURL = event.data.photoURL; const response = await fetch(photoURL); const buffer = await response.arrayBuffer(); const storagePath = `user/${email}/profile.jpeg`; const storageRef = ref(storage, storagePath); const metadata = { contentType: "image/jpeg", }; const snapshot = await uploadBytesResumable( storageRef, buffer, metadata ); if (snapshot.state === "success") { const user = await User.create({ name, email, photoPathFirestore: storagePath, }); console.log("Successfully created new user in DB!"); return { displayName: user.name, customClaims: { role: "user", userId: user._id, }, }; } else { throw new Error("Error occurred while creating User"); } } } else { if (checkUser) { return { customClaims: { role: "user", }, }; } return { customClaims: { role: "org", }, }; } } catch (err) { throw new Error(err); } }); export const beforesignin = beforeUserSignedIn(async (event) => { try { const email = event.data.email; const checkUser = await User.findOne({ email }); if (checkUser) { return { displayName: checkUser.name, customClaims: { role: "user", userId: checkUser._id, }, }; } else { const checkOrg = await Organization.findOne({ email }); return { displayName: checkOrg.name, customClaims: { role: "org", orgId: checkOrg._id, }, }; } } catch (err) { console.log(err); throw new Error(err); } });
3. MongoDB用户注册API接口/auth/user/register
router.post("/register", async (req, res) => { try { const { name, email, phone } = req.body; if (!name || !email) { throw new Error( "Following fields are mandatory: name, email, password, photo" ); } else { if (!req.files[0]) { throw new Error( "Following fields are mandatory: name, email, password, photo" ); } const file = req.files[0]; const filename = file.originalname; const extension = filename.split(".").pop(); if ( extension !== "png" && extension !== "jpg" && extension !== "jpeg" ) { response_400( res, "Invalid file format. Only .jpg, .png, .jpeg files are allowed" ); } else { Jimp.read(file.buffer) .then((image) => { return image .quality(100) .getBufferAsync(Jimp.MIME_JPEG); }) .then(async (jpegBuffer) => { console.log("Image converted successfully!"); const pathToFile = `user/${email}/profile.jpeg`; const storageRef = ref(storage, pathToFile); const metadata = { contentType: "image/jpeg", }; const snapshot = await uploadBytesResumable( storageRef, jpegBuffer, metadata ); if (snapshot.state === "success") { const user = await User.create({ name, email, photoPathFirestore: pathToFile, phone, }); console.log("Successfully created new user in DB!"); response_200( res, "Successfully created new user in DB" ); } else { response_500( res, "Error occurred while uploading user file" ); } }) .catch((err) => { console.error("Error converting image:", err); response_500(res, "Error converting image"); }); } } } catch (err) { console.log(err); res.send(err); } });
问题排查与解决步骤
1. 定位错误根源
auth/error-code:-47本质是Firebase Auth的beforeUserCreated或beforeUserSignedIn触发器执行失败导致的——这类触发器是同步执行的,一旦抛出未处理异常,就会阻断Auth流程并返回这个模糊错误码。
2. 修复触发器中的逻辑错误
(1) 修正beforecreated的引用错误
当检测到checkOrg存在时,代码错误地引用了checkUser的属性,改为:
// 原错误代码 return { displayName: checkUser.name, customClaims: { role: "org", orgId: checkUser._id, }, }; // 修正后 return { displayName: checkOrg.name, customClaims: { role: "org", orgId: checkOrg._id, }, };
(2) 修复beforesignin的空值风险
当checkUser和checkOrg都不存在时,访问checkOrg.name会抛出异常,添加空值判断:
if (checkUser) { return { displayName: checkUser.name, customClaims: { role: "user", userId: checkUser._id, }, }; } else { const checkOrg = await Organization.findOne({ email }); if (!checkOrg) { throw new Error(`No user or organization found for email: ${email}`); } return { displayName: checkOrg.name, customClaims: { role: "org", orgId: checkOrg._id, }, }; }
3. 优化错误日志
在Firebase Functions的异常捕获中输出详细错误,便于调试:
catch (err) { console.error("Trigger execution failed:", err.stack); throw new Error(`Auth trigger error: ${err.message}`); }
前端启用Auth调试日志:
import { getAuth, setLogLevel } from "firebase/auth"; const auth = getAuth(); setLogLevel('debug');
4. 调整注册流程的原子性
当前先写MongoDB再创建Auth账户的流程,若Auth创建失败会产生孤立数据。建议改为:
- 先调用
createUserWithEmailAndPassword创建Auth账户 - 成功后再调用API写入MongoDB
或使用MongoDB事务确保两者一致性。
5. 完善Google登录的图片处理逻辑
Google登录分支中直接下载photoURL可能遇到跨域或资源不可访问的情况,添加响应校验:
const response = await fetch(photoURL); if (!response.ok) { throw new Error(`Failed to fetch profile photo: ${response.statusText}`); } const buffer = await response.arrayBuffer();
内容的提问来源于stack exchange,提问作者BlazeRod11

