多因素认证
多因素认证(MFA),有时也称为双因素认证(2FA),通过额外的验证步骤来确认用户身份,为您的应用程序增加了一层安全防护。
在应用程序中使用MFA被视为最佳实践。
使用弱密码或社交登录账户被泄露的用户容易遭受恶意账户接管。MFA可以防止这种情况,因为它要求用户同时提供以下两种证明:
- 用户知道的信息 密码,或社交登录账户的访问权限
- 用户拥有的设备 对验证器应用(即TOTP)或手机的访问权限
概述
Supabase Auth 通过两种方式实现多因素认证(MFA):
- 应用验证器 - 使用基于时间的一次性密码(TOTP)
- 手机短信验证 - 使用由 Supabase Auth 生成的验证码
使用 MFA 的应用需要实现两个重要流程:
- 注册流程 允许用户在您的应用中设置和管理 MFA
- 认证流程 允许用户在常规登录步骤后使用任意已注册的因素进行验证
Supabase Auth 提供以下功能:
- 注册API - 用于构建添加/删除验证因素的丰富用户界面
- 挑战与验证API - 安全验证用户是否拥有对某个因素的访问权限
- 列出因素API - 用于构建使用附加因素登录的丰富用户界面
您可以通过 Supabase 仪表板控制对注册API以及挑战与验证API的访问权限。设置为Verification Disabled
将同时禁用挑战API和验证API。
这些API集合让您能够定制适合自身需求的MFA体验。您可以创建可选MFA、全员强制MFA或仅针对特定用户组强制MFA的流程。
当用户完成因素注册或使用因素登录后,Supabase Auth 会在用户的访问令牌(JWT)中添加额外的元数据,您的应用可以利用这些数据来允许或拒绝访问。
这些信息通过认证保证级别(Authenticator Assurance Level)表示,这是衡量Supabase Auth对特定会话中用户身份验证保证程度的标准指标。目前有两个公认级别:
-
保证级别1:
aal1
表示用户身份通过常规登录方式验证,如邮箱+密码、魔法链接、一次性密码、手机认证或社交登录。 -
保证级别2:
aal2
表示用户身份通过至少一个第二因素进行了额外验证,如TOTP代码或一次性密码代码。
这个保证级别会被编码到用户关联JWT的aal
声明中。通过解码这个值,您可以在前端、后端和数据库中创建自定义授权规则,以强制执行适合您应用的MFA策略。没有aal
声明的JWT默认为aal1
级别。
在应用中添加MFA
为应用添加多因素认证(MFA)需要以下四个步骤:
-
添加注册流程 您需要在应用中提供用户界面,让用户能够设置MFA。可以在注册后立即添加,或作为应用设置部分的独立流程。
-
添加取消注册流程 您需要支持一个用户界面,让用户可以查看现有设备并取消注册不再相关的设备。
-
在登录中添加验证步骤 如果用户设置了MFA,应用的登录流程需要向用户展示验证界面,要求他们证明拥有额外因素的访问权限。
-
强制执行MFA登录规则 当用户能够注册并使用MFA登录后,您需要在应用的各个层面强制执行授权规则:包括前端、后端、API服务器或行级安全策略。
注册流程和验证步骤因认证因素而异,具体内容请参阅单独页面。访问手机验证或应用验证器页面查看如何为相应因素添加流程。您可以组合这两种流程,允许同时使用手机和应用验证器因素。
添加取消注册流程
取消注册流程对于手机和TOTP因素都是相同的。
取消注册流程为用户提供了一个管理界面,可以查看并取消与其账户关联的认证因素。大多数应用通过因素管理页面实现此功能,用户可以在该页面查看并取消关联选定的因素。
当用户取消注册某个因素时,调用supabase.auth.mfa.unenroll()
并传入因素ID。例如:
1...({ : 'd30fd651-184e-4748-a928-0a4b9be1d429' })
这将取消注册ID为d30fd651-184e-4748-a928-0a4b9be1d429
的因素。
强制执行 MFA 登录规则
仅在应用界面中添加 MFA(多因素认证)功能本身并不能为用户提供更高的安全性。您还需要在应用的数据库、API 和服务器端渲染中强制执行 MFA 规则。
根据应用需求,您可以选择以下三种方式来强制执行 MFA:
-
对所有用户强制执行(新用户和现有用户) 任何用户账户都必须注册 MFA 才能继续使用您的应用。 应用将不允许未通过 MFA 验证的访问。
-
仅对新用户强制执行 只有新用户会被强制要求注册 MFA,而现有用户会被鼓励(而非强制)启用。 应用将不允许新用户在未通过 MFA 验证的情况下访问。
-
仅对选择启用的用户强制执行 希望使用 MFA 的用户可以自行注册,应用将不允许这些用户在未通过 MFA 验证的情况下访问。
示例:React
以下示例创建了一个新的 UnenrollMFA
组件,展示了 MFA 注销流程的关键部分。请注意,用户只有在完成注册流程并获得 aal2
JWT 声明后才能注销认证因素。以下是需要注意的几点:
- 当组件出现在屏幕上时,
supabase.auth.mfa.listFactors()
端点会获取所有现有因素及其详细信息 - 用户的现有因素会显示在表格中
- 用户选择要注销的因素后,可以输入
factorId
并点击 注销 按钮,这将创建一个确认模态框
注销因素后,只有在刷新间隔到期后,保障级别才会从 aal2
降级到 aal1
。若要在注册后立即从 aal2
降级到 aal1
,需要手动调用 refreshSession()
123456789101112131415161718192021222324252627282930313233343536373839404142434445/** * UnenrollMFA 组件显示一个简单表格,列出所有认证因素以及注销按钮。 * 当用户输入希望注销的因素的 factorId 并点击注销时,相应的因素将被注销。 */export function UnenrollMFA() { const [factorId, setFactorId] = useState('') const [factors, setFactors] = useState([]) const [error, setError] = useState('') // 存储错误信息 useEffect(() => { ;(async () => { const { data, error } = await supabase.auth.mfa.listFactors() if (error) { throw error } setFactors([...data.totp, ...data.phone]) })() }, []) return ( <> {error && <div className="error">{error}</div>} <tbody> <tr> <td>因素ID</td> <td>友好名称</td> <td>因素状态</td> <td>电话号码</td> </tr> {factors.map((factor) => ( <tr> <td>{factor.id}</td> <td>{factor.friendly_name}</td> <td>{factor.factor_type}</td> <td>{factor.status}</td> <td>{factor.phone}</td> </tr> ))} </tbody> <input type="text" value={verifyCode} onChange={(e) => setFactorId(e.target.value.trim())} /> <button onClick={() => supabase.auth.mfa.unenroll({ factorId })}>注销</button> </> )}
数据库
您的应用程序应根据用户当前及可能的认证级别,充分拒绝或允许对表或行的访问。
Postgres 有两种策略类型:宽松型(permissive)和限制型(restrictive)。本指南使用限制型策略。请确保不要遗漏 as restrictive
子句。
对所有用户强制执行(新用户和现有用户)
如果您的应用属于这种情况,这是一个可以应用于所有表的行级安全策略模板:
12345create policy "策略名称" on 表名 as restrictive to authenticated using ((select auth.jwt()->>'aal') = 'aal2');
- 此策略将不接受任何
aal
声明值不为aal2
的 JWT,这是最高的认证保证级别。 - 使用
as restrictive
确保此策略将限制表上的所有命令,无论其他策略如何!
仅对新用户强制执行
如果您的应用属于这种情况,规则会变得更加复杂。在特定时间戳之后创建的用户帐户必须具有 aal2
级别才能访问数据库。
12345678910111213create policy "策略名称" on 表名 as restrictive -- 非常重要! to authenticated using (array[(select auth.jwt()->>'aal')] <@ ( select case when created_at >= '2022-12-12T00:00:00Z' then array['aal2'] else array['aal1', 'aal2'] end as aal from auth.users where (select auth.uid()) = id));
- 对于创建时间戳在 2022 年 12 月 12 日 00:00 UTC 之前的用户,该策略将接受
aal1
和aal2
,但对于其他时间戳,仅接受aal2
。 <@
操作符是 PostgreSQL 的"包含于"操作符。- 使用
as restrictive
确保此策略将限制表上的所有命令,无论其他策略如何!
仅对已启用MFA的用户强制执行
已在其账户上启用多因素认证(MFA)的用户期望您的应用程序仅在他们完成MFA验证后才允许访问。
1234567891011121314create policy "策略名称" on 表名 as restrictive -- 非常重要! to authenticated using ( array[(select auth.jwt()->>'aal')] <@ ( select case when count(id) > 0 then array['aal2'] else array['aal1', 'aal2'] end as aal from auth.mfa_factors where ((select auth.uid()) = user_id) and status = 'verified' ));
- 当用户至少有一个已验证的MFA因素时,该策略将仅接受
aal2
级别的认证 - 否则,它将同时接受
aal1
和aal2
级别的认证 <@
操作符是PostgreSQL的"包含于"数组操作符- 使用
as restrictive
确保此策略将限制表上的所有命令,无论其他策略如何!
服务端渲染
在服务端渲染环境中使用 Supabase JavaScript 库时,请确保为每个请求都创建一个新对象!这样可以避免意外渲染和提供属于不同用户的内容。
可以在服务端渲染层面强制执行 MFA(多因素认证)。但要做到这一点可能会比较复杂。
您可以使用 supabase.auth.mfa.getAuthenticatorAssuranceLevel()
和 supabase.auth.mfa.listFactors()
API 来识别会话的 AAL(认证保证等级)级别以及用户启用的任何认证因素,这与在浏览器中使用这些 API 的方式类似。
然而,在服务器上遇到不同的 AAL 级别实际上可能并不是安全问题。考虑以下常见场景:
- 用户使用常规方式登录,但在 MFA 流程中关闭了标签页
- 用户长时间忘记关闭标签页(这种情况比您想象的更常见)
- 用户丢失了认证设备,对下一步操作感到困惑
因此,我们建议将用户重定向到一个可以让他们使用附加因素进行认证的页面,而不是直接渲染 HTTP 401 未授权或 HTTP 403 禁止访问的内容。
API 保护
如果您的应用程序使用 Supabase 数据库、存储或边缘函数,仅使用行级安全策略就能提供足够的保护。如果您还有其他需要保护的 API,请遵循以下通用准则:
-
为您的编程语言选择可靠的 JWT 验证和解析库 这将帮助您安全地解析 JWT 并提取其中的声明信息。
-
从 JWT 中获取
aal
声明并根据需求比较其值 如果遇到可提升的 AAL 等级,应要求用户继续登录流程而非直接登出。 -
使用
https://<project-ref>.supabase.co/rest/v1/auth/factors
REST 端点来识别用户是否注册了任何 MFA 因素 只有状态为verified
(已验证)的因素才应被纳入考量。