基于Firebase Blocking Functions实现自定义双因素认证(2FA)
自定义Firebase 2FA方案的安全问询
Firebase身份平台默认仅支持短信类双因素认证(2FA),对其他2FA方式的支持有限,因此我们实现了一套自定义2FA方案:
实现流程
- 通过Blocking Functions处理邮箱密码登录,在
beforeSignIn函数中判断用户是否需要认证器类型的2FA:
const beforeSignIn = functions .auth.user() .beforeSignIn(async (user, context) => { const userAuth = await admin.firestore().collection("user").doc(user.uid).get() if (userAuth.data().multifactorType === 'authenticator') { console.log(`User [${user.uid}] requires 2FA with authenticator`); const loginAttempt = {eventId: context.eventId, timestamp: Timestamp.now()} await admin.firestore().collection('user').doc(user.uid).update({multifactorLoginAttempt: loginAttempt}) const hmac = crypto.createHmac('sha256', 'top-secret-key').update(user.uid).update(context.eventId).digest('hex'); throw new functions.auth.HttpsError('failed-precondition', JSON.stringify({ uid: user.uid, hmac })); } console.log(`No 2FA in db. User will login`); });
- 用户收到包含HMAC和UID的错误信息后,被重定向到OTP输入页面
- 前端携带UID、HMAC和OTP调用后端
/api/verify-multifactor接口,验证通过后生成自定义令牌让用户登录:
multifactorRoutes.post( '/verify', async ( req: Request<{}, {}, MultifactorVerifyType>, res: Response<{ token: string } | { message: string }> ) => { const userId = req.body.uid; const user = await auth.firestore().collection("user").doc(userId).get(); if (!user.multifactorLoginAttempt) { res.status(401).send({ message: 'No existing login attempt' }); return; } const hmacRecreated = crypto.createHmac('sha256', 'top-secret-key').update(userId).update(user.multifactorLoginEvent.eventId).digest('hex'); const isExpectedHmac = crypto.timingSafeEqual( Buffer.from(hmacRecreated), Buffer.from(req.body.hmac) ); if (!isExpectedHmac) { res.status(401).send({ message: 'Failed to verify hmac' }); return; } /** using otplib **/ const isValidOtp = authenticator.verify({ token: req.body.token, secret: user.secret, }); if (isValidOtp) { const token = await admin.auth().createCustomToken(userId); await admin.firestore().collection('user').doc(userId).update({ multifactorLoginAttempt: deleteValue() }); res.status(200).send({ token }); } else { res.status(401).send({ message: 'Illegal token' }); } } );
技术疑问
- 我们的
/api/verify-multifactor端点是否还需要额外安全措施?目前认为HMAC+OTP能防暴力破解,但想确认是否有遗漏。 - 有没有其他方式从Firebase Blocking Functions获取临时令牌,替代自制的HMAC?
安全建议与替代方案
一、/api/verify-multifactor端点的额外安全措施
现有方案已具备基础安全性,可补充以下措施强化防护:
- 限制登录尝试有效期:给
multifactorLoginAttempt添加过期时间(比如15分钟),验证时先检查时间戳是否过期,避免攻击者利用旧HMAC长时间尝试破解OTP。 - OTP错误次数限制:记录用户连续输错OTP的次数,达到阈值(比如5次)就锁定当前登录尝试,甚至临时冻结用户2FA验证权限一段时间,防止暴力破解OTP。
- 强制HTTPS传输:确保所有前端到
/verify接口的请求都通过HTTPS传输,避免HMAC、OTP等敏感数据被中间人截获。 - 清理冗余记录:定期清理Firestore中已过期或已完成的
multifactorLoginAttempt,减少攻击者可利用的旧数据。 - 限制并发登录尝试:在
beforeSignIn中更新记录时,检查用户是否已有未过期的登录尝试,存在则覆盖或拒绝新请求,避免同一用户同时存在多个待验证会话。
二、替代自制HMAC的临时令牌方案
可利用Firebase内置能力或规范工具生成临时令牌,替代自制HMAC:
- 使用Firebase会话Cookie:在
beforeSignIn中不抛出错误,而是调用createSessionCookie生成短期会话Cookie返回给前端。前端携带Cookie和OTP调用验证接口,后端先验证Cookie有效性,再校验OTP,无需自行实现HMAC逻辑。 - 生成签名JWT:用JWT库生成包含
uid、eventId和过期时间的签名JWT,密钥托管在Google Secret Manager中。后端验证JWT的签名和有效期,替代HMAC验证,JWT自带的过期和签名机制比自制HMAC更规范。 - 使用Firebase动态链接:生成包含
uid和eventId的加密动态链接,用户通过链接进入OTP输入页面,前端从链接提取参数后调用验证接口,动态链接的加密由Firebase处理,避免敏感参数暴露。
内容的提问来源于stack exchange,提问作者KimHafr
相关产品推荐
相关产品推荐

