多因素认证(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界面来设置额外的身份验证因素。大多数应用会在两个地方添加注册流程:
-
登录或注册后立即显示 这允许用户在登录或创建账户后快速设置MFA(多因素认证)。如果适合您的应用场景,我们建议鼓励所有用户设置MFA。许多应用会将其作为可选步骤提供,以减少新用户注册的阻力。
-
在设置页面中提供 允许用户设置、禁用或修改他们的MFA配置。
为MFA注册一个认证因素需要三个步骤:
-
调用
supabase.auth.mfa.enroll()
方法 该方法会返回一个二维码和一个密钥。向用户显示二维码并要求他们使用认证器应用扫描。如果用户无法扫描二维码,则以纯文本形式显示密钥,用户可以手动输入或粘贴到他们的认证器应用中。 -
调用
supabase.auth.mfa.challenge()
API 这一步会准备Supabase Auth以接受用户的验证码,并返回一个挑战ID。对于手机MFA,此步骤还会向用户发送验证码。 -
调用
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
按钮
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576/** * 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)。
因此,如果您收到的 currentLevel
是 aal1
但 nextLevel
是 aal2
,则应该为用户提供进行多因素认证(MFA)的选项。
下表解释了这些组合的含义:
当前等级 | 下一等级 | 含义 |
---|---|---|
aal1 | aal1 | 用户未注册多因素认证。 |
aal1 | aal2 | 用户已注册多因素认证但尚未验证。 |
aal2 | aal2 | 用户已验证其多因素认证。 |
aal2 | aal1 | 用户已禁用其多因素认证。(过期的 JWT) |
示例: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
组件。
以下是实现验证和校验逻辑的组件:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253function 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
组件。