为您的 iOS 应用添加 TOTP 多重身份验证

如果您已升级到 Firebase Authentication with Identity Platform,可以向应用添加基于时间的动态密码 (TOTP) 多重身份验证 (MFA)。

借助 Firebase Authentication with Identity Platform,您可以将 TOTP 用作 MFA 的其他因素。启用此功能后,尝试登录您的应用的用户会看到系统要求提供 TOTP。要生成 TOTP,用户必须使用可生成有效 TOTP 代码的身份验证器应用,例如 Google 身份验证器

准备工作

  1. 请至少启用一个支持 MFA 的提供方。请注意,除下列情况之外的所有提供方都支持 MFA:

    • 电话身份验证
    • 匿名身份验证
    • 自定义身份验证令牌
    • Apple 游戏中心
  2. 确保您的应用验证用户电子邮件地址。MFA 要求验证电子邮件地址。这样可以防止恶意操作者使用别人的电子邮件地址注册服务,然后通过添加第二重身份验证阻止实际的电子邮件地址所有者注册。

  3. 如果您尚未安装 Firebase Apple SDK,请进行安装。

    只有 iOS 上的 Apple SDK v10.12.0 版及更高版本支持 TOTP MFA。

启用 TOTP MFA

如需启用 TOTP 作为第二重身份验证,请使用 Admin SDK 或调用项目配置 REST 端点。

如需使用 Admin SDK,请执行以下操作:

  1. 如果您尚未安装 Firebase Admin Node.js SDK,请进行安装。

    只有 Firebase Admin Node.js SDK 11.6.0 版及更高版本支持 TOTP MFA。

  2. 请运行以下命令:

    import { getAuth } from 'firebase-admin/auth';
    
    getAuth().projectConfigManager().updateProjectConfig(
    {
          multiFactorConfig: {
              providerConfigs: [{
                  state: "ENABLED",
                  totpProviderConfig: {
                      adjacentIntervals: {
                          NUM_ADJ_INTERVALS
                      },
                  }
              }]
          }
    })
    

    替换以下内容:

    • NUM_ADJ_INTERVALS:接受 TOTP 的相邻时间范围间隔数(从 0 到 10)。默认值为 5。

      TOTP 的工作原理是确保两方(证明器和验证器)在同一时间范围内(通常为 30 秒)生成动态密码时,两者会生成相同的密码。但是,为了适应各方和人工响应时间之间的时钟偏移,您可以将 TOTP 服务配置为也接受相邻时间范围的 TOTP。

如需使用 REST API 启用 TOTP MFA,请运行以下代码:

curl -X PATCH "https://identitytoolkit.googleapis.com/admin/v2/projects/PROJECT_ID/config?updateMask=mfa" \
    -H "Authorization: Bearer $(gcloud auth print-access-token)" \
    -H "Content-Type: application/json" \
    -H "X-Goog-User-Project: PROJECT_ID" \
    -d \
    '{
        "mfa": {
          "providerConfigs": [{
            "state": "ENABLED",
            "totpProviderConfig": {
              "adjacentIntervals": "NUM_ADJ_INTERVALS"
            }
          }]
       }
    }'

请替换以下内容:

  • PROJECT_ID:项目 ID。
  • NUM_ADJ_INTERVALS:时间范围间隔数(从 0 到 10)。默认值为 5。

    TOTP 的工作原理是确保两方(证明器和验证器)在同一时间范围内(通常为 30 秒)生成动态密码时,两者会生成相同的密码。但是,为了适应各方和人工响应时间之间的时钟偏移,您可以将 TOTP 服务配置为也接受相邻时间范围的 TOTP。

选择注册模式

您可以选择应用是否要求多重身份验证,以及何时和如何注册用户。一些常见模式包括:

  • 在注册过程中注册用户的第二重身份验证。如果应用要求所有用户进行多重身份验证,请使用此方法。

  • 提供可在注册期间跳过第二重身份验证注册的选项。如果您想要建议但不要求在应用中使用多重身份验证,可以使用此方法。

  • 提供从用户的账号或个人资料管理页面(而不是注册界面)添加第二重身份验证的功能。这样可以使注册过程更顺畅,同时仍可为注重安全的用户提供多重身份验证。

  • 如果用户希望访问安全性要求更高的功能,再要求添加第二重身份验证。

在 TOTP MFA 中注册用户

启用 TOTP MFA 作为应用的第二重身份验证后,请实现客户端逻辑以在 TOTP MFA 中注册用户:

  1. 重新验证用户身份。

  2. 为经过身份验证的用户生成 TOTP 密文:

    // Generate a TOTP secret.
    guard let mfaSession = try? await currentUser.multiFactor.session() else { return }
    guard let totpSecret = try? await TOTPMultiFactorGenerator.generateSecret(with: mfaSession) else { return }
    
    // Display the secret to the user and prompt them to enter it into their
    // authenticator app. (See the next step.)
    
  3. 向用户显示密文并提示他们将其输入到身份验证器应用中:

    // Display this key:
    let secret = totpSecret.sharedSecretKey()
    

    除了显示密钥之外,您还可以尝试将其自动添加到设备的默认身份验证器应用中。为此,请生成与 Google 身份验证器兼容的密钥 URI,并将其传递给 openInOTPApp(withQRCodeURL:)

    let otpAuthUri = totpSecret.generateQRCodeURL(
        withAccountName: currentUser.email ?? "default account",
        issuer: "Your App Name")
    totpSecret.openInOTPApp(withQRCodeURL: otpAuthUri)
    

    用户将其密文添加到身份验证器应用中后,应用将开始生成 TOTP。

  4. 提示用户输入身份验证器应用显示的 TOTP,并使用它来完成 MFA 注册:

    // Ask the user for a verification code from the authenticator app.
    let verificationCode = // Code from user input.
    
    // Finalize the enrollment.
    let multiFactorAssertion = TOTPMultiFactorGenerator.assertionForEnrollment(
        with: totpSecret,
        oneTimePassword: verificationCode)
    do {
        try await currentUser.multiFactor.enroll(
            with: multiFactorAssertion,
            displayName: "TOTP")
    } catch {
        // Wrong or expired OTP. Re-prompt the user.
    }
    

让用户通过第二重身份验证登录

如需让用户通过 TOTP MFA 登录,请使用以下代码:

  1. 像未使用 MFA 时一样调用一种 signIn(with...:) 方法(例如 signIn(withEmail:password:))。如果该方法抛出错误且代码为 secondFactorRequired,请启动应用的 MFA 流程。

    do {
        let authResult = try await Auth.auth().signIn(withEmail: email, password: password)
    
        // If the user is not enrolled with a second factor and provided valid
        // credentials, sign-in succeeds.
    
        // (If your app requires MFA, this could be considered an error
        // condition, which you would resolve by forcing the user to enroll a
        // second factor.)
    
        // ...
    } catch let error as AuthErrorCode where error.code == .secondFactorRequired {
        // Initiate your second factor sign-in flow. (See next step.)
        // ...
    } catch {
        // Other auth error.
        throw error
    }
    
  2. 应用的 MFA 流程应首先提示用户选择想要使用的第二重身份验证。您可以通过检查 MultiFactorResolver 实例的 hints 属性来获取受支持的第二重身份验证列表:

    let mfaKey = AuthErrorUserInfoMultiFactorResolverKey
    guard let resolver = error.userInfo[mfaKey] as? MultiFactorResolver else { return }
    let enrolledFactors = resolver.hints.map(\.displayName)
    
  3. 如果用户选择使用 TOTP,请提示他们输入身份验证器应用中显示的 TOTP,然后使用该 TOTP 登录:

    let multiFactorInfo = resolver.hints[selectedIndex]
    switch multiFactorInfo.factorID {
    case TOTPMultiFactorID:
        let otpFromAuthenticator = // OTP typed by the user.
        let assertion = TOTPMultiFactorGenerator.assertionForSignIn(
            withEnrollmentID: multiFactorInfo.uid,
            oneTimePassword: otpFromAuthenticator)
        do {
            let authResult = try await resolver.resolveSignIn(with: assertion)
        } catch {
            // Wrong or expired OTP. Re-prompt the user.
        }
    default:
        return
    }
    

取消注册 TOTP MFA

本部分介绍如何处理用户取消注册 TOTP MFA 的情况。

如果用户注册了多个 MFA 选项,并且用户取消注册了最近启用的选项,则会接收到 auth/user-token-expired 并退出账号。用户必须重新登录并验证其现有凭据,例如电子邮件地址和密码。

如需取消注册用户、处理错误并触发重新身份验证,请使用以下代码:

guard let currentUser = Auth.auth().currentUser else { return }

// Prompt the user to select a factor to unenroll, from this array:
currentUser.multiFactor.enrolledFactors

// ...

// Unenroll the second factor.
let multiFactorInfo = currentUser.multiFactor.enrolledFactors[selectedIndex]
do {
    try await currentUser.multiFactor.unenroll(with: multiFactorInfo)
} catch let error as AuthErrorCode where error.code == .invalidUserToken {
    // Second factor unenrolled, but the user was signed out. Re-authenticate
    // them.
}

后续步骤