认证

多因素认证(手机)


手机多因素认证如何工作?

手机多因素认证(MFA)涉及由Supabase Auth和终端用户共同生成的共享验证码。该验证码通过短信或WhatsApp等消息渠道发送,用户使用该验证码向Supabase Auth进行认证。

MFA的手机消息配置与手机登录认证共享。用于手机登录的相同提供商配置也适用于MFA。如果您需要使用不同于原生支持的MFA(手机)消息提供商,还可以使用发送短信钩子

下图展示了在MFA(手机)场景中,注册(Enrollment)和验证(Verify)API的工作流程。

添加注册流程

注册流程为用户提供了一个UI界面来设置额外的认证因素。大多数应用程序会在两个位置添加注册流程:

  1. 登录或注册后立即提供 这允许用户在登录或创建账户后快速设置多因素认证(MFA)。尽可能鼓励所有用户设置MFA。许多应用程序将此作为可选步骤,以减少用户注册时的摩擦。

  2. 在设置页面中提供 允许用户设置、禁用或修改他们的MFA设置。

尽可能保持一个通用流程,只需稍作修改即可在这两种情况下重复使用。

为MFA注册一个因素(以手机MFA为例)需要三个步骤:

  1. 调用 supabase.auth.mfa.enroll()
  2. 调用 supabase.auth.mfa.challenge() API。这会通过短信或WhatsApp发送验证码,并准备Supabase Auth接受用户输入的验证码。
  3. 调用 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 按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
export 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)。

因此,如果您收到的 currentLevelaal1nextLevelaal2,则应为用户提供进行多因素认证(MFA)的选项。

下表解释了不同组合的含义:

当前等级下一等级含义
aal1aal1用户未注册多因素认证。
aal1aal2用户已注册多因素认证但未完成验证。
aal2aal2用户已完成多因素认证验证。
aal2aal1用户已禁用其多因素认证(令牌已过期)。

示例:React

在登录流程中添加验证步骤很大程度上取决于您的应用架构。不过,React 应用中一种较为常见的结构是使用一个大型组件(通常命名为 App)来包含大部分认证逻辑。

以下示例将用验证逻辑包裹该组件,在必要时显示 MFA 验证界面,然后再展示完整应用。如下方 AppWithMFA 示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function 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 组件。

以下是实现验证和验证码逻辑的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function 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使用量