在 iOS 應用程式中加入 TOTP 多重驗證

如果已升級至 Firebase Authentication with Identity Platform,您可以在應用程式中新增限時動態密碼 (TOTP) 多重驗證 (MFA)。

Firebase Authentication with Identity Platform 可讓您使用 TOTP 做為 MFA 的額外因素。啟用這項功能後,嘗試登入應用程式的使用者會看到 TOTP 要求。如要產生驗證碼,使用者必須使用可產生有效 TOTP 驗證碼的驗證器應用程式,例如 Google Authenticator

事前準備

  1. 啟用至少一個支援 MFA 的供應商。請注意,下列供應商除外,所有供應商都支援多重驗證:

    • 電話驗證
    • 匿名驗證
    • 自訂驗證權杖
    • Apple Game Center
  2. 確保應用程式會驗證使用者電子郵件地址。多重驗證需要電子郵件驗證。這麼做可防止惡意人士使用不屬於自己的電子郵件地址註冊服務,然後新增第二個驗證因素,將電子郵件地址的實際擁有者排除在外。

  3. 如果您尚未安裝 Firebase Apple SDK,請先安裝。

    TOTP MFA 僅支援Apple SDK 10.12.0 以上版本,且僅適用於 iOS。

啟用 TOTP 多重驗證

如要啟用 TOTP 做為次要驗證方法,請使用 Admin SDK 或呼叫專案設定 REST 端點。

如要使用 Admin SDK,請按照下列步驟操作:

  1. 如果尚未安裝,請安裝 Firebase Admin Node.js SDK

    TOTP MFA 僅支援 Firebase Admin Node.js SDK 11.6.0 以上版本。

  2. 執行以下指令:

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

    更改下列內容:

    • NUM_ADJ_INTERVALS:可接受 TOTP 的相鄰時間視窗間隔數,範圍為零到十。預設值為 5。

      TOTP 的運作方式是確保兩方 (驗證者和驗證器) 在同一時間範圍 (通常為 30 秒) 內產生 OTP 時,會產生相同的密碼。不過,為因應雙方時鐘不同步的情況,以及人類的回應時間,您可以設定 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:時間範圍間隔數,從零到十。預設值為五。

    TOTP 的運作方式是確保兩方 (驗證者和驗證器) 在同一時間範圍 (通常為 30 秒) 內產生 OTP 時,會產生相同的密碼。不過,為因應雙方時鐘不同步的情況,以及人類的回應時間,您可以設定 TOTP 服務,讓系統也接受相鄰時間範圍內的 TOTP。

選擇註冊模式

您可以選擇應用程式是否需要多重驗證,以及如何和何時註冊使用者。常見的模式包括:

  • 在註冊程序中,為使用者註冊第二重驗證。如果應用程式要求所有使用者都必須進行多重驗證,請使用這個方法。

  • 在註冊期間提供可略過的選項,讓使用者註冊第二個驗證因素。如果您想鼓勵使用者在應用程式中啟用多重驗證,但並非強制要求,或許可以採用這種做法。

  • 提供從使用者帳戶或個人資料管理頁面新增第二個驗證因素的功能,而非註冊畫面。這樣可盡量減少註冊程序中的阻礙,同時仍為注重安全性的使用者提供多重驗證功能。

  • 當使用者想存取安全性要求較高的功能時,逐步要求新增第二個驗證因素。

為使用者啟用 TOTP 多重驗證

為應用程式啟用 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 Authenticator 相容的金鑰 URI,然後傳遞至 openInOTPApp(withQRCodeURL:)

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

    使用者將密碼新增至驗證器應用程式後,應用程式就會開始產生 TOTP。

  4. 提示使用者輸入驗證器應用程式顯示的 TOTP,並使用該 TOTP 完成多重驗證註冊:

    // 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 多重驗證登入使用者,請使用下列程式碼:

  1. 呼叫其中一個 signIn(with...:) 方法,就像您未使用 MFA 時一樣 (例如 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,並用來登入:

    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 多重驗證

本節說明如何處理使用者取消註冊 TOTP 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.
}

後續步驟