多因素认证(手机)
手机多因素认证如何工作?
手机多因素认证(MFA)涉及由Supabase Auth和终端用户共同生成的共享验证码。该验证码通过短信或WhatsApp等消息渠道发送,用户使用该验证码向Supabase Auth进行认证。
MFA的手机消息配置与手机登录认证共享。用于手机登录的相同提供商配置也适用于MFA。如果您需要使用不同于原生支持的MFA(手机)消息提供商,还可以使用发送短信钩子。
下图展示了在MFA(手机)场景中,注册(Enrollment)和验证(Verify)API的工作流程。
添加注册流程
注册流程为用户提供了一个UI界面来设置额外的认证因素。大多数应用程序会在两个位置添加注册流程:
-
登录或注册后立即提供 这允许用户在登录或创建账户后快速设置多因素认证(MFA)。尽可能鼓励所有用户设置MFA。许多应用程序将此作为可选步骤,以减少用户注册时的摩擦。
-
在设置页面中提供 允许用户设置、禁用或修改他们的MFA设置。
尽可能保持一个通用流程,只需稍作修改即可在这两种情况下重复使用。
为MFA注册一个因素(以手机MFA为例)需要三个步骤:
- 调用
supabase.auth.mfa.enroll()
- 调用
supabase.auth.mfa.challenge()
API。这会通过短信或WhatsApp发送验证码,并准备Supabase Auth接受用户输入的验证码。 - 调用
supabase.auth.mfa.verify()
API。supabase.auth.mfa.challenge()
会返回一个挑战ID。 这将验证Supabase Auth发出的代码是否与用户输入的代码匹配。如果验证成功,该因素会立即对用户账户生效。如果失败,您应该重复步骤2和3。
示例:React
以下是一个创建新 EnrollMFA
组件的示例,展示了 MFA 注册流程的关键部分:
- 当组件出现在屏幕上时,会调用一次
supabase.auth.mfa.enroll()
API 来开始为当前用户注册新验证因子的流程 - 使用
supabase.auth.mfa.challenge()
API 创建挑战,并通过supabase.auth.mfa.verify()
提交用户输入的验证码进行验证 onEnabled
是一个回调函数,用于通知其他组件注册已完成onCancelled
是一个回调函数,用于通知其他组件用户已点击Cancel
按钮
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384export function EnrollMFA({ onEnrolled, onCancelled,}: { onEnrolled: () => void onCancelled: () => void}) { const [phoneNumber, setPhoneNumber] = useState('') const [factorId, setFactorId] = useState('') const [verifyCode, setVerifyCode] = useState('') const [error, setError] = useState('') const [challengeId, setChallengeId] = useState('') const onEnableClicked = () => { setError('') ;(async () => { const verify = await auth.mfa.verify({ factorId, challengeId, code: verifyCode, }) if (verify.error) { setError(verify.error.message) throw verify.error } onEnrolled() })() } const onEnrollClicked = async () => { setError('') try { const factor = await auth.mfa.enroll({ phone: phoneNumber, factorType: 'phone', }) if (factor.error) { setError(factor.error.message) throw factor.error } setFactorId(factor.data.id) } catch (error) { setError('注册验证因子失败') } } const onSendOTPClicked = async () => { setError('') try { const challenge = await auth.mfa.challenge({ factorId }) if (challenge.error) { setError(challenge.error.message) throw challenge.error } setChallengeId(challenge.data.id) } catch (error) { setError('重新发送验证码失败') } } return ( <> {error && <div className="error">{error}</div>} <input type="text" placeholder="手机号码" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value.trim())} /> <input type="text" placeholder="验证码" value={verifyCode} onChange={(e) => setVerifyCode(e.target.value.trim())} /> <input type="button" value="注册" onClick={onEnrollClicked} /> <input type="button" value="提交验证码" onClick={onEnableClicked} /> <input type="button" value="发送OTP验证码" onClick={onSendOTPClicked} /> <input type="button" value="取消" onClick={onCancelled} /> </> )}
为登录流程添加验证步骤
当用户通过第一因素(邮箱+密码、魔法链接、一次性密码、社交登录等)完成登录后,您需要检查是否还需要验证其他因素。
这可以通过调用 supabase.auth.mfa.getAuthenticatorAssuranceLevel()
API 实现。当用户登录并重定向回您的应用时,应当调用此方法来获取用户当前及下一步的身份验证保障等级(AAL)。
因此,如果您收到的 currentLevel
是 aal1
而 nextLevel
是 aal2
,则应为用户提供进行多因素认证(MFA)的选项。
下表解释了不同组合的含义:
当前等级 | 下一等级 | 含义 |
---|---|---|
aal1 | aal1 | 用户未注册多因素认证。 |
aal1 | aal2 | 用户已注册多因素认证但未完成验证。 |
aal2 | aal2 | 用户已完成多因素认证验证。 |
aal2 | aal1 | 用户已禁用其多因素认证(令牌已过期)。 |
示例:React
在登录流程中添加验证步骤很大程度上取决于您的应用架构。不过,React 应用中一种较为常见的结构是使用一个大型组件(通常命名为 App
)来包含大部分认证逻辑。
以下示例将用验证逻辑包裹该组件,在必要时显示 MFA 验证界面,然后再展示完整应用。如下方 AppWithMFA
示例所示:
123456789101112131415161718192021222324252627282930313233function AppWithMFA() { const [readyToShow, setReadyToShow] = useState(false) const [showMFAScreen, setShowMFAScreen] = useState(false) useEffect(() => { ;(async () => { try { const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel() if (error) { throw error } console.log(data) if (data.nextLevel === 'aal2' && data.nextLevel !== data.currentLevel) { setShowMFAScreen(true) } } finally { setReadyToShow(true) } })() }, []) if (readyToShow) { if (showMFAScreen) { return <AuthMFA /> } return <App /> } return <></>}
supabase.auth.mfa.getAuthenticatorAssuranceLevel()
确实返回一个 Promise。 无需担心,这个方法执行极快(微秒级),因为它很少需要网络请求。readyToShow
仅用于确保在向用户展示任何界面之前完成 AAL 检查。- 如果当前级别可以升级到下一级别,则显示 MFA 验证界面。
- 验证成功后,最终会在屏幕上渲染
App
组件。
以下是实现验证和验证码逻辑的组件:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172function AuthMFA() { const [verifyCode, setVerifyCode] = useState('') const [error, setError] = useState('') const [factorId, setFactorId] = useState('') const [challengeId, setChallengeId] = useState('') const [phoneNumber, setPhoneNumber] = useState('') const startChallenge = async () => { setError('') try { const factors = await supabase.auth.mfa.listFactors() if (factors.error) { throw factors.error } const phoneFactor = factors.data.phone[0] if (!phoneFactor) { throw new Error('未找到手机验证因素!') } const factorId = phoneFactor.id setFactorId(factorId) setPhoneNumber(phoneFactor.phone) const challenge = await supabase.auth.mfa.challenge({ factorId }) if (challenge.error) { setError(challenge.error.message) throw challenge.error } setChallengeId(challenge.data.id) } catch (error) { setError(error.message) } } const verifyCode = async () => { setError('') try { const verify = await supabase.auth.mfa.verify({ factorId, challengeId, code: verifyCode, }) if (verify.error) { setError(verify.error.message) throw verify.error } } catch (error) { setError(error.message) } } return ( <> <div>请输入发送到您手机的验证码。</div> {phoneNumber && <div>手机号码:{phoneNumber}</div>} {error && <div className="error">{error}</div>} <input type="text" value={verifyCode} onChange={(e) => setVerifyCode(e.target.value.trim())} /> {!challengeId ? ( <input type="button" value="开始验证" onClick={startChallenge} /> ) : ( <input type="button" value="验证代码" onClick={verifyCode} /> )} </> )}
- 通过调用
supabase.auth.mfa.listFactors()
可以获取用户的可用 MFA 验证因素。这个方法同样执行迅速且很少需要网络请求。 - 如果
listFactors()
返回多个验证因素(或不同类型),您应该让用户进行选择。为简化示例,此处未展示此逻辑。 - 手机号码对每个用户具有唯一性。用户只能使用一个已验证的手机号码因素。尝试使用相同号码注册新的手机因素会导致错误。
- 每次用户点击"提交"按钮时,都会为选定的因素(本例中是第一个因素)创建新的验证挑战。
- 验证成功后,客户端库会自动在后台刷新会话,并最终调用
onSuccess
回调,从而在屏幕上显示已认证的App
组件。
安全配置
每个验证码有效期为5分钟,过期后可发送新验证码。连续发送的验证码在到期前均保持有效。在适用场景下,请尽可能选择最长的验证码长度(最少6位)。您可以在认证设置中进行配置。
请注意,手机多因素认证(MFA)容易受到SIM卡交换攻击,攻击者会致电移动运营商要求将目标手机号转移至新SIM卡,然后使用该SIM卡拦截MFA验证码。请评估您的应用程序对此类攻击的容忍度。您可在此了解更多关于SIM卡交换攻击的信息
计费方案
第一个项目每小时$0.1027(每月$75)。每增加一个项目,每小时$0.0137(每月$10)。
套餐 | 第一个项目每月费用 | 第二个项目每月费用 | 第三个项目每月费用 |
---|---|---|---|
专业版 | $75 | $10 | $10 |
团队版 | $75 | $10 | $10 |
企业版 | 定制 | 定制 | 定制 |
如需了解费用计算的详细说明,请参阅管理高级手机MFA使用量。