多因素认证(TOTP)
应用身份验证器的多因素认证是怎么工作的? #
🌐 How does app authenticator multi-factor authentication work?
应用认证器(TOTP)多因素认证涉及一个由用户控制的认证器应用生成的定时一次性密码。它使用二维码来传输用于生成一次性密码的共享密钥。用户可以用他们的手机扫描二维码,从而获取后续认证所需的共享密钥。
🌐 App Authenticator (TOTP) multi-factor authentication involves a timed one-time password generated from an authenticator app in the control of users. It uses a QR Code which to transmit a shared secret used to generate a One Time Password. A user can scan a QR code with their phone to capture a shared secret required for subsequent authentication.
二维码最初是由 Google Authenticator 引入的,但现在所有身份验证应用都普遍接受它。二维码还有一种替代形式,就是遵循 otpauth 方案的 URI,比如:otpauth://totp/supabase:alice@supabase.com?secret=<secret>&issuer=supabase,当二维码无法显示时,用户可以手动输入这个 URI。
下面是一个流程图,说明了在多因素认证(TOTP)的背景下,注册、挑战和验证 API 是如何工作的。
🌐 Below is a flow chart illustrating how the Enrollment, Challenge, and Verify APIs work in the context of MFA (TOTP).
在设置流程中,一个已经达到 AAL1 的会话调用 Enroll API,这会返回一个二维码供用户用身份验证器应用扫描。用户输入生成的代码后,Challenge 和 Verify API 会进行验证,如果成功,会话就升级到 AAL2。如果代码错误,用户会被提示重新输入。
🌐 In the setup flow, a session already at AAL1 calls the Enroll API, which returns a QR code for the user to scan with their authenticator app. The user enters the generated code, the Challenge and Verify APIs check it, and on success the session is upgraded to AAL2. If the code is incorrect, the user is prompted to enter it again.
在登录流程中,用户登录(将会话升级到 AAL1),然后调用 List Factors API。如果用户有一个或多个因素,他们会打开身份验证器并输入代码,这会遵循相同的挑战和验证路径以达到 AAL2。如果他们没有注册任何因素,则会先引导他们进入设置流程。
🌐 In the login flow, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they open their authenticator and enter a code, which follows the same Challenge and Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first.
TOTP MFA API 免费使用,并且在所有 Supabase 项目中默认启用。
添加注册流程 #
🌐 Add enrollment flow
注册流程提供一个用户界面,让用户设置额外的身份验证因素。大多数应用会在应用中的两个地方添加注册流程:
🌐 An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
- 登录或注册后立即进行。这可以让用户在登录或创建账户后立即设置多因素认证(MFA)。我们建议如果适合你的应用,鼓励所有用户设置MFA。许多应用将其作为自愿步骤提供,以降低入门摩擦。
- 在设置页面内。允许用户设置、禁用或修改他们的多因素认证(MFA)设置。
注册一个多因素认证使用的因素需要三个步骤:
🌐 Enrolling a factor for use with MFA takes three steps:
- 调用
supabase.auth.mfa.enroll()。这个方法会返回一个二维码和一个密钥。把二维码展示给用户,让他们用认证器应用扫描。如果他们无法扫描二维码,就显示明文密钥,他们可以手动输入或粘贴到认证器应用中。 - 正在调用
supabase.auth.mfa.challenge()API。这会让 Supabase Auth 准备好接收用户的验证码,并返回一个挑战 ID。在多因素认证(MFA)使用手机的情况下,这一步还会把验证码发送给用户。 - 正在调用
supabase.auth.mfa.verify()API。这是为了验证用户是否确实已经将第(1)步中的密钥添加到他们的应用中,并且运行正常。如果验证成功,该因素会立即对用户账户生效。如果验证失败,你需要重复第 2 步和第 3 步。
示例:React #
🌐 Example: React
下面是一个示例,它创建了一个新的 EnrollMFA 组件,展示了多因素认证注册流程中的重要部分。
🌐 Below is an example that creates a new EnrollMFA component that illustrates the important pieces of the MFA enrollment flow.
- 当组件出现在屏幕上时,
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 * EnrollMFA shows an enrollment dialog. When shown on screen it calls3 * the `enroll` API. Each time a user clicks the Enable button it calls the4 * `challenge` and `verify` APIs to check if the code provided by the user is5 * valid.6 * When enrollment is successful, it calls `onEnrolled`. When the user clicks7 * Cancel the `onCancelled` callback is called.8 */9export function EnrollMFA({10 onEnrolled,11 onCancelled,12}: {13 onEnrolled: () => void14 onCancelled: () => void15}) {16 const [factorId, setFactorId] = useState('')17 const [qr, setQR] = useState('') // holds the QR code image SVG18 const [verifyCode, setVerifyCode] = useState('') // contains the code entered by the user19 const [error, setError] = useState('') // holds an error message2021 const onEnableClicked = () => {22 setError('')23 ;(async () => {24 const challenge = await supabase.auth.mfa.challenge({ factorId })25 if (challenge.error) {26 setError(challenge.error.message)27 throw challenge.error28 }2930 const challengeId = challenge.data.id3132 const verify = await supabase.auth.mfa.verify({33 factorId,34 challengeId,35 code: verifyCode,36 })37 if (verify.error) {38 setError(verify.error.message)39 throw verify.error40 }4142 onEnrolled()43 })()44 }4546 useEffect(() => {47 ;(async () => {48 const { data, error } = await supabase.auth.mfa.enroll({49 factorType: 'totp',50 })51 if (error) {52 throw error53 }5455 setFactorId(data.id)5657 // Supabase Auth returns an SVG QR code which you can convert into a data58 // URL that you can place in an <img> tag.59 setQR(data.totp.qr_code)60 })()61 }, [])6263 return (64 <>65 {error && <div className="error">{error}</div>}66 <img src={qr} />67 <input68 type="text"69 value={verifyCode}70 onChange={(e) => setVerifyCode(e.target.value.trim())}71 />72 <input type="button" value="Enable" onClick={onEnableClicked} />73 <input type="button" value="Cancel" onClick={onCancelled} />74 </>75 )76}在登录时添加一个挑战步骤 #
🌐 Add a challenge step to login
一旦用户通过他们的第一步验证登录(邮箱+密码、魔法链接、一次性密码、社交登录等),你需要检查是否需要验证其他额外的因素。
🌐 Once a user has logged in via their first factor (email+password, magic link, one time password, social login etc.) you need to perform a check if any additional factors need to be verified.
这可以通过使用 supabase.auth.mfa.getAuthenticatorAssuranceLevel() API 来完成。当用户登录并被重定向回你的应用时,你应该调用这个方法来提取用户当前和下一个认证保证等级 (AAL)。
🌐 This can be done by using the supabase.auth.mfa.getAuthenticatorAssuranceLevel() API. When the user signs in and is redirected back to your app, you should call this method to extract the user's current and next authenticator assurance level (AAL).
因此,如果你收到一个 currentLevel,它是 aal1,但却是 nextLevel 的 aal2,用户应该有机会选择进行多因素认证(MFA)。
🌐 Therefore if you receive a currentLevel which is aal1 but a nextLevel of aal2, the user should be given the option to go through MFA.
下面有一张表格,说明了组合含义。
🌐 Below is a table that explains the combined meaning.
| 当前等级 | 下一等级 | 含义 |
|---|---|---|
aal1 | aal1 | 用户未注册 MFA。 |
aal1 | aal2 | 用户已注册 MFA 但未验证。 |
aal2 | aal2 | 用户已验证其 MFA。 |
aal2 | aal1 | 用户已禁用其 MFA。(过期的 JWT。) |
示例:React #
🌐 Example: React
在登录时添加挑战步骤在很大程度上取决于你的应用架构。不过,一个相当常见的做法是将 React 应用结构化,通常是有一个大组件(经常命名为 App),其中包含大部分经过身份验证的应用逻辑。
🌐 Adding the challenge step to login depends heavily on the architecture of your app. However, a fairly common way to structure React apps is to have a large component (often named App) which contains most of the authenticated application logic.
这个例子会在显示完整应用之前,用逻辑把这个组件封装起来,如果有必要,会显示多因素认证(MFA)挑战屏幕。下面的 AppWithMFA 例子中有说明。
🌐 This example will wrap this component with logic that will show an MFA challenge screen if necessary, before showing the full application. This is illustrated in the AppWithMFA example below.
1function AppWithMFA() {2 const [readyToShow, setReadyToShow] = useState(false)3 const [showMFAScreen, setShowMFAScreen] = useState(false)45 useEffect(() => {6 ;(async () => {7 try {8 const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()9 if (error) {10 throw error11 }1213 console.log(data)1415 if (data.nextLevel === 'aal2' && data.nextLevel !== data.currentLevel) {16 setShowMFAScreen(true)17 }18 } finally {19 setReadyToShow(true)20 }21 })()22 }, [])2324 if (readyToShow) {25 if (showMFAScreen) {26 return <AuthMFA />27 }2829 return <App />30 }3132 return <></>33}supabase.auth.mfa.getAuthenticatorAssuranceLevel()确实会返回一个 promise。别担心,这个方法非常快(微秒级),因为它很少使用网络。readyToShow只是确保 AAL 检查在向用户显示任何应用界面之前完成。- 如果当前级别可以升级到下一级,就会显示多因素认证(MFA)屏幕。
- 一旦挑战成功,
App组件就会最终显示在屏幕上。
下面是实现挑战和验证逻辑的组件。
🌐 Below is the component that implements the challenge and verify logic.
1function AuthMFA() {2 const [verifyCode, setVerifyCode] = useState('')3 const [error, setError] = useState('')45 const onSubmitClicked = () => {6 setError('')7 ;(async () => {8 const factors = await supabase.auth.mfa.listFactors()9 if (factors.error) {10 throw factors.error11 }1213 const totpFactor = factors.data.totp[0]1415 if (!totpFactor) {16 throw new Error('No TOTP factors found!')17 }1819 const factorId = totpFactor.id2021 const challenge = await supabase.auth.mfa.challenge({ factorId })22 if (challenge.error) {23 setError(challenge.error.message)24 throw challenge.error25 }2627 const challengeId = challenge.data.id2829 const verify = await supabase.auth.mfa.verify({30 factorId,31 challengeId,32 code: verifyCode,33 })34 if (verify.error) {35 setError(verify.error.message)36 throw verify.error37 }38 })()39 }4041 return (42 <>43 <div>Please enter the code from your authenticator app.</div>44 {error && <div className="error">{error}</div>}45 <input46 type="text"47 value={verifyCode}48 onChange={(e) => setVerifyCode(e.target.value.trim())}49 />50 <input type="button" value="Submit" onClick={onSubmitClicked} />51 </>52 )53}- 你可以通过调用
supabase.auth.mfa.listFactors()来获取用户可用的多因素认证因素。别担心,这个方法也很快,而且很少会使用网络。 - 如果
listFactors()返回多个因子(或类型不同),你应该给用户一个选择。为了简便,这个例子中没有展示。 - 每次用户点击“提交”按钮,都会为选定的因素(在本例中是第一个因素)创建一个新的挑战,并立即进行验证。任何错误都会显示给用户。
- 验证成功后,客户端库会自动在后台刷新会话,最后调用
onSuccess回调,这会在屏幕上显示已认证的App组件。
常见问题 #
🌐 Frequently asked questions
TOTP码有效多久? #
🌐 How long is the TOTP code valid for?
在我们的 TOTP 实现中,每个生成的验证码在一个时间间隔内有效,这个间隔为 30 秒。为了应对轻微的时间差异,我们允许一个时间间隔的时钟偏差。这确保了用户即使系统时钟有些差异,也能在这个时间内成功认证。
🌐 In our TOTP implementation, each generated code remains valid for one interval, which spans 30 seconds. To account for minor time discrepancies, we allow for a one-interval clock skew. This ensures that users can successfully authenticate within this timeframe, even if there are slight variations in system clocks.