认证

多因素认证(TOTP)


应用验证器多因素认证如何工作?

应用验证器(TOTP)多因素认证涉及用户控制的验证器应用生成的定时一次性密码。它使用二维码传输用于生成一次性密码的共享密钥。用户可以用手机扫描二维码获取后续认证所需的共享密钥。

二维码的使用最初由Google Authenticator引入,现已被所有验证器应用普遍接受。二维码还有一个遵循otpauth方案的URI形式替代表示,例如:otpauth://totp/supabase:alice@supabase.com?secret=<secret>&issuer=supabase,在二维码渲染困难时用户可以手动输入。

下图展示了在MFA(TOTP)上下文中,注册(Enrollment)、挑战(Challenge)和验证(Verify)API的工作流程。

TOTP MFA API可免费使用,默认在所有Supabase项目中启用。

添加注册流程

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

  1. 登录或注册后立即显示 这允许用户在登录或创建账户后快速设置MFA(多因素认证)。如果适合您的应用场景,我们建议鼓励所有用户设置MFA。许多应用会将其作为可选步骤提供,以减少新用户注册的阻力。

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

为MFA注册一个认证因素需要三个步骤:

  1. 调用supabase.auth.mfa.enroll()方法 该方法会返回一个二维码和一个密钥。向用户显示二维码并要求他们使用认证器应用扫描。如果用户无法扫描二维码,则以纯文本形式显示密钥,用户可以手动输入或粘贴到他们的认证器应用中。

  2. 调用supabase.auth.mfa.challenge() API 这一步会准备Supabase Auth以接受用户的验证码,并返回一个挑战ID。对于手机MFA,此步骤还会向用户发送验证码。

  3. 调用supabase.auth.mfa.verify() API 这一步验证用户是否确实将步骤(1)中的密钥添加到了他们的应用中且工作正常。如果验证成功,该因素会立即对用户账户生效。如果失败,您应该重复步骤2和3。

示例:React

下面是一个创建新 EnrollMFA 组件的示例,展示了 MFA 注册流程的关键部分:

  • 当组件出现在屏幕上时,会调用一次 supabase.auth.mfa.enroll() API 来开始为当前用户注册新验证因素
  • 该 API 返回 SVG 格式的二维码,通过将 SVG 编码为数据 URL 后使用普通的 <img> 标签显示在屏幕上
  • 用户使用身份验证器应用扫描二维码后,应在 verifyCode 输入字段中输入验证码并点击 Enable
  • 使用 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
/** * EnrollMFA 组件显示一个简单的注册对话框。当显示在屏幕上时,它会调用 * `enroll` API。每次用户点击 Enable 按钮时,它会调用 * `challenge` 和 `verify` API 来检查用户提供的代码是否 * 有效。 * 当注册成功时,会调用 `onEnrolled`。当用户点击 * Cancel 按钮时,会调用 `onCancelled` 回调。 */export function EnrollMFA({ onEnrolled, onCancelled,}: { onEnrolled: () => void onCancelled: () => void}) { const [factorId, setFactorId] = useState('') const [qr, setQR] = useState('') // 保存二维码图片 SVG const [verifyCode, setVerifyCode] = useState('') // 包含用户输入的代码 const [error, setError] = useState('') // 保存错误信息 const onEnableClicked = () => { setError('') ;(async () => { const challenge = await supabase.auth.mfa.challenge({ factorId }) if (challenge.error) { setError(challenge.error.message) throw challenge.error } const challengeId = challenge.data.id const verify = await supabase.auth.mfa.verify({ factorId, challengeId, code: verifyCode, }) if (verify.error) { setError(verify.error.message) throw verify.error } onEnrolled() })() } useEffect(() => { ;(async () => { const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp', }) if (error) { throw error } setFactorId(data.id) // Supabase Auth 返回一个 SVG 二维码,您可以将其转换为数据 // URL 并放置在 <img> 标签中 setQR(data.totp.qr_code) })() }, []) return ( <> {error && <div className="error">{error}</div>} <img src={qr} /> <input type="text" value={verifyCode} onChange={(e) => setVerifyCode(e.target.value.trim())} /> <input type="button" value="Enable" onClick={onEnableClicked} /> <input type="button" value="Cancel" onClick={onCancelled} /> </> )}

添加登录验证步骤

当用户通过第一因素(邮箱+密码、魔法链接、一次性密码、社交登录等)登录后,您需要检查是否还需要验证其他因素。

这可以通过使用 supabase.auth.mfa.getAuthenticatorAssuranceLevel() API 实现。当用户登录并重定向回您的应用时,您应该调用此方法来获取用户当前和下一步的身份验证保证等级(AAL)。

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

下表解释了这些组合的含义:

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

示例: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
function AuthMFA() { const [verifyCode, setVerifyCode] = useState('') const [error, setError] = useState('') const onSubmitClicked = () => { setError('') ;(async () => { const factors = await supabase.auth.mfa.listFactors() if (factors.error) { throw factors.error } const totpFactor = factors.data.totp[0] if (!totpFactor) { throw new Error('No TOTP factors found!') } const factorId = totpFactor.id const challenge = await supabase.auth.mfa.challenge({ factorId }) if (challenge.error) { setError(challenge.error.message) throw challenge.error } const challengeId = challenge.data.id const verify = await supabase.auth.mfa.verify({ factorId, challengeId, code: verifyCode, }) if (verify.error) { setError(verify.error.message) throw verify.error } })() } return ( <> <div>请输入您验证器应用中的代码。</div> {error && <div className="error">{error}</div>} <input type="text" value={verifyCode} onChange={(e) => setVerifyCode(e.target.value.trim())} /> <input type="button" value="提交" onClick={onSubmitClicked} /> </> )}
  • 通过调用 supabase.auth.mfa.listFactors() 可以获取用户的可用 MFA 验证因素。这个方法同样执行迅速且很少需要网络请求。
  • 如果 listFactors() 返回多个验证因素(或不同类型),您应该让用户进行选择。为简化示例,此处未展示该逻辑。
  • 每次用户点击"提交"按钮时,都会为选定的验证因素(本例中为第一个)创建新的验证挑战并立即校验。任何错误都会显示给用户。
  • 验证成功后,客户端库会自动在后台刷新会话,最终调用 onSuccess 回调,在屏幕上显示已认证的 App 组件。

常见问题