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

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

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

事前準備

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

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

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

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

啟用 TOTP MFA

如要啟用 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:時間範圍間隔數量,從 0 到 10。預設為五。

    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)
    

    使用者將 Secret 新增至驗證器應用程式後,應用程式就會開始產生 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. 呼叫其中一個 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. 應用程式的多重驗證流程應先提示使用者選擇要使用的第二個驗證方式。如要取得支援的第二驗證因素清單,請檢查 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.
}

後續步驟