多因素认证(手机)
手机多因素认证是怎么工作的? #
🌐 How does phone multi-factor-authentication work?
手机多因素认证涉及由 Supabase Auth 和终端用户生成的共享代码。代码通过消息渠道发送,比如短信或 WhatsApp,用户使用该代码来认证 Supabase Auth。
🌐 Phone multi-factor authentication involves a shared code generated by Supabase Auth and the end user. The code is delivered via a messaging channel, such as SMS or WhatsApp, and the user uses the code to authenticate to Supabase Auth.
用于多因素认证(MFA)的手机消息配置与手机验证登录共享。用于手机登录的同一供应商配置也用于MFA。如果你需要使用与系统原生支持不同的MFA(手机)消息供应商,你也可以使用发送短信钩子。
🌐 The phone messaging configuration for MFA is shared with phone auth login. The same provider configuration that is used for phone login is used for MFA. You can also use the Send SMS Hook if you need to use an MFA (Phone) messaging provider different from what is supported natively.
下面是一个流程图,展示了在多因素认证(手机)的情况下,注册和验证 API 是如何工作的。
🌐 Below is a flow chart illustrating how the Enrollment and Verify APIs work in the context of MFA (Phone).
在设置流程中,一个已经处于 AAL1 的会话会先调用 Enroll API,然后调用 Challenge API,这会通过短信或 WhatsApp 向用户发送验证码。用户输入验证码后,Verify API 会进行验证,如果成功,会话就会升级到 AAL2。输入错误的验证码会让用户回到输入验证码的步骤。
🌐 In the setup flow, a session already at AAL1 calls the Enroll API followed by the Challenge API, which sends a code to the user over SMS or WhatsApp. The user enters the code, the Verify API checks it, and on success the session is upgraded to AAL2. An incorrect code returns the user to the code-entry step.
在登录流程中,用户登录(将会话升级到 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 select their phone factor and enter the code that was sent, following the same 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:
- 登录或注册后立即。这样用户可以在登录或创建账户后设置多重身份验证(MFA)。如果可能的话,鼓励所有用户设置MFA。许多应用将其作为可选择的步骤,以减少用户首次使用的阻力。
- 在设置页面内。允许用户设置、禁用或修改他们的多因素认证(MFA)设置。
尽量保持一个通用的流程,这样稍作修改就可以在两种情况下重复使用。
🌐 As far as possible, maintain a generic flow that you can reuse in both cases with minor modifications.
为电话多因素认证 (MFA) 注册一个因素有三个步骤:
🌐 Enrolling a factor for use with MFA takes three steps for phone MFA:
- 调用
supabase.auth.mfa.enroll()。 - 正在调用
supabase.auth.mfa.challenge()API。这会通过短信或 WhatsApp 发送一个验证码,并让 Supabase Auth 准备好接受用户输入的验证码。 - 调用
supabase.auth.mfa.verify()API。supabase.auth.mfa.challenge()会返回一个挑战 ID。这用于验证 Supabase Auth 发出的代码是否与用户输入的代码匹配。如果验证成功,该因素会立即对用户账户生效。如果验证失败,你需要重复步骤 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 会被调用一次,以开始为当前用户注册一个新的验证方式。 - 一个挑战是使用
supabase.auth.mfa.challenge()API 创建的,用户的代码通过supabase.auth.mfa.verify()挑战提交进行验证。 onEnabled是一个回调,用来通知其他组件注册已完成。onCancelled是一个回调,用来通知其他组件用户点击了Cancel按钮。
1export function EnrollMFA({2 onEnrolled,3 onCancelled,4}: {5 onEnrolled: () => void6 onCancelled: () => void7}) {8 const [phoneNumber, setPhoneNumber] = useState('')9 const [factorId, setFactorId] = useState('')10 const [verifyCode, setVerifyCode] = useState('')11 const [error, setError] = useState('')12 const [challengeId, setChallengeId] = useState('')1314 const onEnableClicked = () => {15 setError('')16 ;(async () => {17 const verify = await auth.mfa.verify({18 factorId,19 challengeId,20 code: verifyCode,21 })22 if (verify.error) {23 setError(verify.error.message)24 throw verify.error25 }2627 onEnrolled()28 })()29 }30 const onEnrollClicked = async () => {31 setError('')32 try {33 const factor = await auth.mfa.enroll({34 phone: phoneNumber,35 factorType: 'phone',36 })37 if (factor.error) {38 setError(factor.error.message)39 throw factor.error40 }4142 setFactorId(factor.data.id)43 } catch (error) {44 setError('Failed to Enroll the Factor.')45 }46 }4748 const onSendOTPClicked = async () => {49 setError('')50 try {51 const challenge = await auth.mfa.challenge({ factorId })52 if (challenge.error) {53 setError(challenge.error.message)54 throw challenge.error55 }5657 setChallengeId(challenge.data.id)58 } catch (error) {59 setError('Failed to resend the code.')60 }61 }6263 return (64 <>65 {error && <div className="error">{error}</div>}66 <input67 type="text"68 placeholder="Phone Number"69 value={phoneNumber}70 onChange={(e) => setPhoneNumber(e.target.value.trim())}71 />72 <input73 type="text"74 placeholder="Verification Code"75 value={verifyCode}76 onChange={(e) => setVerifyCode(e.target.value.trim())}77 />78 <input type="button" value="Enroll" onClick={onEnrollClicked} />79 <input type="button" value="Submit Code" onClick={onEnableClicked} />80 <input type="button" value="Send OTP Code" onClick={onSendOTPClicked} />81 <input type="button" value="Cancel" onClick={onCancelled} />82 </>83 )84}在登录时添加一个挑战步骤 #
🌐 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('')4 const [factorId, setFactorId] = useState('')5 const [challengeId, setChallengeId] = useState('')6 const [phoneNumber, setPhoneNumber] = useState('')78 const startChallenge = async () => {9 setError('')10 try {11 const factors = await supabase.auth.mfa.listFactors()12 if (factors.error) {13 throw factors.error14 }1516 const phoneFactor = factors.data.phone[0]1718 if (!phoneFactor) {19 throw new Error('No phone factors found!')20 }2122 const factorId = phoneFactor.id23 setFactorId(factorId)24 setPhoneNumber(phoneFactor.phone)2526 const challenge = await supabase.auth.mfa.challenge({ factorId })27 if (challenge.error) {28 setError(challenge.error.message)29 throw challenge.error30 }3132 setChallengeId(challenge.data.id)33 } catch (error) {34 setError(error.message)35 }36 }3738 const verifyCode = async () => {39 setError('')40 try {41 const verify = await supabase.auth.mfa.verify({42 factorId,43 challengeId,44 code: verifyCode,45 })46 if (verify.error) {47 setError(verify.error.message)48 throw verify.error49 }50 } catch (error) {51 setError(error.message)52 }53 }5455 return (56 <>57 <div>Please enter the code sent to your phone.</div>58 {phoneNumber && <div>Phone number: {phoneNumber}</div>}59 {error && <div className="error">{error}</div>}60 <input61 type="text"62 value={verifyCode}63 onChange={(e) => setVerifyCode(e.target.value.trim())}64 />65 {!challengeId ? (66 <input type="button" value="Start Challenge" onClick={startChallenge} />67 ) : (68 <input type="button" value="Verify Code" onClick={verifyCode} />69 )}70 </>71 )72}- 你可以通过调用
supabase.auth.mfa.listFactors()来获取用户可用的多因素认证因素。别担心,这个方法也很快,而且很少会使用网络。 - 如果
listFactors()返回多个因子(或类型不同),你应该给用户一个选择。为了简便,这个例子中没有展示。 - 每个用户的电话号码都是唯一的。用户在一个电话号码下只能有一个已验证的电话验证方式。如果尝试在已有同一号码的已验证验证方式的情况下注册新的电话验证方式,就会出错。
- 每次用户按下“提交”按钮时,都会为所选因素(在这种情况下是第一个因素)创建一个新的挑战
- 验证成功后,客户端库会自动在后台刷新会话,最后调用
onSuccess回调,这会在屏幕上显示已认证的App组件。
安全设置 #
🌐 Security configuration
每个代码有效期最多为5分钟,之后可以发送新的代码。连续的代码在过期前仍然有效。尽可能选择对你的使用场景来说最长可接受的代码长度,最少为6位。这个可以在认证设置中进行配置。
🌐 Each code is valid for up to 5 minutes, after which a new one can be sent. Successive codes remain valid until expiry. When possible choose the longest code length acceptable to your use case, at a minimum of 6. This can be configured in the Authentication Settings.
请注意,手机多因素认证(MFA)容易受到SIM卡交换攻击。在这种攻击中,攻击者会联系移动运营商,要求将目标的电话号码转到一张新的SIM卡上,然后使用该SIM卡拦截MFA验证码。评估一下你的应用对这种攻击的容忍度。你可以在这里了解更多关于SIM卡交换攻击的信息。
🌐 Be aware that Phone MFA is vulnerable to SIM swap attacks where an attacker will call a mobile provider and ask to port the target's phone number to a new SIM card and then use the said SIM card to intercept an MFA code. Evaluate the your application's tolerance for such an attack. You can read more about SIM swapping attacks here
价格 #
🌐 Pricing
$0.1027 每小时($75 每月)用于第一个项目。 $0.0137 每小时($10 每月)用于每个额外项目。
| 计划 | 每月项目 1 | 每月项目 2 | 每月项目 3 |
|---|---|---|---|
| 高级 | $75 | $10 | $10 |
| 团队 | $75 | $10 | $10 |
| 企业 | 定制 | 定制 | 定制 |
有关费用如何计算的详细分解,请参阅管理高级 MFA 电话使用。
🌐 For a detailed breakdown of how charges are calculated, refer to Manage Advanced MFA Phone usage.