Skip to content
Auth

多因素认证(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).

Yes No 1 or more factors 0 factors Setup flow /Session is AAL1/ Enroll API Show QR code User: Scan QR code in authenticator User: Enter code Challenge + Verify API Is code correct? /Upgrade to AAL2/ Done Login flow User: Sign-in /Upgrade to AAL1/ List Factors API User: Open authenticator Setup flow

设置流程中,一个已经达到 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.

添加注册流程 #

🌐 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:

  1. 登录或注册后立即进行。这可以让用户在登录或创建账户后立即设置多因素认证(MFA)。我们建议如果适合你的应用,鼓励所有用户设置MFA。许多应用将其作为自愿步骤提供,以降低入门摩擦。
  2. 在设置页面内。允许用户设置、禁用或修改他们的多因素认证(MFA)设置。

注册一个多因素认证使用的因素需要三个步骤:

🌐 Enrolling a factor for use with MFA takes three steps:

  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 #

🌐 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 calls
3
* the `enroll` API. Each time a user clicks the Enable button it calls the
4
* `challenge` and `verify` APIs to check if the code provided by the user is
5
* valid.
6
* When enrollment is successful, it calls `onEnrolled`. When the user clicks
7
* Cancel the `onCancelled` callback is called.
8
*/
9
export function EnrollMFA({
10
onEnrolled,
11
onCancelled,
12
}: {
13
onEnrolled: () => void
14
onCancelled: () => void
15
}) {
16
const [factorId, setFactorId] = useState('')
17
const [qr, setQR] = useState('') // holds the QR code image SVG
18
const [verifyCode, setVerifyCode] = useState('') // contains the code entered by the user
19
const [error, setError] = useState('') // holds an error message
20
21
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.error
28
}
29
30
const challengeId = challenge.data.id
31
32
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.error
40
}
41
42
onEnrolled()
43
})()
44
}
45
46
useEffect(() => {
47
;(async () => {
48
const { data, error } = await supabase.auth.mfa.enroll({
49
factorType: 'totp',
50
})
51
if (error) {
52
throw error
53
}
54
55
setFactorId(data.id)
56
57
// Supabase Auth returns an SVG QR code which you can convert into a data
58
// URL that you can place in an <img> tag.
59
setQR(data.totp.qr_code)
60
})()
61
}, [])
62
63
return (
64
<>
65
{error && <div className="error">{error}</div>}
66
<img src={qr} />
67
<input
68
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,但却是 nextLevelaal2,用户应该有机会选择进行多因素认证(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.

当前等级下一等级含义
aal1aal1用户未注册 MFA。
aal1aal2用户已注册 MFA 但未验证。
aal2aal2用户已验证其 MFA。
aal2aal1用户已禁用其 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.

1
function AppWithMFA() {
2
const [readyToShow, setReadyToShow] = useState(false)
3
const [showMFAScreen, setShowMFAScreen] = useState(false)
4
5
useEffect(() => {
6
;(async () => {
7
try {
8
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
9
if (error) {
10
throw error
11
}
12
13
console.log(data)
14
15
if (data.nextLevel === 'aal2' && data.nextLevel !== data.currentLevel) {
16
setShowMFAScreen(true)
17
}
18
} finally {
19
setReadyToShow(true)
20
}
21
})()
22
}, [])
23
24
if (readyToShow) {
25
if (showMFAScreen) {
26
return <AuthMFA />
27
}
28
29
return <App />
30
}
31
32
return <></>
33
}
  • supabase.auth.mfa.getAuthenticatorAssuranceLevel() 确实会返回一个 promise。别担心,这个方法非常快(微秒级),因为它很少使用网络。
  • readyToShow 只是确保 AAL 检查在向用户显示任何应用界面之前完成。
  • 如果当前级别可以升级到下一级,就会显示多因素认证(MFA)屏幕。
  • 一旦挑战成功,App 组件就会最终显示在屏幕上。

下面是实现挑战和验证逻辑的组件。

🌐 Below is the component that implements the challenge and verify logic.

1
function AuthMFA() {
2
const [verifyCode, setVerifyCode] = useState('')
3
const [error, setError] = useState('')
4
5
const onSubmitClicked = () => {
6
setError('')
7
;(async () => {
8
const factors = await supabase.auth.mfa.listFactors()
9
if (factors.error) {
10
throw factors.error
11
}
12
13
const totpFactor = factors.data.totp[0]
14
15
if (!totpFactor) {
16
throw new Error('No TOTP factors found!')
17
}
18
19
const factorId = totpFactor.id
20
21
const challenge = await supabase.auth.mfa.challenge({ factorId })
22
if (challenge.error) {
23
setError(challenge.error.message)
24
throw challenge.error
25
}
26
27
const challengeId = challenge.data.id
28
29
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.error
37
}
38
})()
39
}
40
41
return (
42
<>
43
<div>Please enter the code from your authenticator app.</div>
44
{error && <div className="error">{error}</div>}
45
<input
46
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.