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

如果您已透過 Identity Platform 升級至 Firebase 驗證,即可將簡訊多重驗證新增至 iOS 應用程式。

多重驗證可提高應用程式的安全性。攻擊者通常也會竊取密碼和社群媒體帳戶,但是要攔截簡訊更困難。

事前準備

  1. 請至少啟用一個支援多重驗證功能的提供者。每個供應商都支援多重驗證,不包括手機驗證、匿名驗證和 Apple Game Center。

  2. 確認應用程式正在驗證使用者的電子郵件地址。必須進行電子郵件驗證。這可以防止惡意人士使用他人擁有的電子郵件註冊服務,並新增第二個因素來鎖定真實的擁有者。

啟用多重驗證

  1. 開啟 Firebase 控制台的「Authentication」>「Sign-in method」頁面。

  2. 在「進階」部分中,啟用「簡訊多重驗證」

    請一併輸入要測試應用程式的電話號碼。 雖然非必要,但強烈建議您註冊測試電話號碼,避免在開發期間發生節流限制。

  3. 如果您尚未授權應用程式的網域,請在 Firebase 控制台的「Authentication」>「Settings」頁面將該網域加入許可清單。

驗證應用程式

Firebase 需要驗證簡訊要求是否來自您的應用程式。驗證方式有兩種:

  • 靜音 APN 通知:當您首次登入使用者時,Firebase 可以傳送無訊息推播通知到使用者的裝置。如果應用程式收到通知,驗證作業就會繼續進行。請注意,從 iOS 8.0 開始,您不需要要求使用者允許推播通知使用這個方法。

  • reCAPTCHA 驗證:如果您無法傳送無訊息通知 (例如使用者停用背景重新整理,或在 iOS 模擬器中測試應用程式),您可以使用 reCAPTCHA。在許多情況下,reCAPTCHA 會自動解決,使用者完全不需要進行互動。

使用靜音通知

如何啟用 APN 通知與 Firebase 搭配使用:

  1. 在 Xcode 中,為專案啟用推播通知

  2. 使用 Firebase 控制台上傳 APN 驗證金鑰 (您所做的變更將自動轉移至 Google Cloud Firebase)。如果您沒有 APN 驗證金鑰,請參閱使用 FCM 設定 APN,瞭解如何取得金鑰。

    1. 開啟 Firebase 控制台

    2. 前往「專案設定」

    3. 選取「雲端通訊」分頁標籤。

    4. 在「APNs authentication key」(APN 驗證金鑰) 下方的「iOS app configuration」(iOS 應用程式設定) 區段中,按一下 [Upload] (上傳)

    5. 選取金鑰。

    6. 新增該金鑰的金鑰 ID。您可以在 Apple Developer Member Center 的「Certificates, Identifiers & Profiles」下方找到金鑰 ID。

    7. 按一下「上傳」

如果您已有 APN 憑證,可改為上傳憑證。

使用 reCAPTCHA 驗證

如何為用戶端 SDK 啟用 reCAPTCHA:

  1. 在 Xcode 中開啟專案設定。

  2. 按一下左側樹狀檢視中的專案名稱。

  3. 從「Targets」(目標) 部分選取您的應用程式。

  4. 選取「資訊」分頁標籤。

  5. 展開「網址類型」部分。

  6. 按一下「+」按鈕。

  7. 在「網址配置」欄位中輸入復原的用戶端 ID。您可以在 GoogleService-Info.plist 設定檔中找到這個值為 REVERSED_CLIENT_ID

完成後,設定應如下所示:

自訂配置

您可以視需要自訂應用程式在顯示 reCAPTCHA 時,顯示 SFSafariViewControllerUIWebView 的方式。方法是建立符合 FIRAuthUIDelegate 通訊協定的自訂類別,並傳送至 verifyPhoneNumber:UIDelegate:completion:

選擇註冊模式

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

  • 註冊使用者的第二重驗證在註冊過程中。如果應用程式需要所有使用者的多重驗證,請使用這個方法。請注意,帳戶必須有已驗證的電子郵件地址才能註冊第二重驗證方式,因此註冊流程必須配合這種方式。

  • 提供可略過的選項,讓使用者在註冊期間註冊第二個步驟。如果應用程式希望鼓勵但不需要多重驗證,則建議使用此方法。

  • 能夠從使用者帳戶或設定檔管理頁面 (而非註冊畫面) 新增第二重驗證方式。這樣可盡量減少註冊過程中的阻礙,同時讓有安全性考量的使用者可以使用多重要素驗證功能。

  • 如果使用者想要存取安全性需求較高的功能,則需要逐步新增第二個驗證步驟。

註冊第二重驗證方式

如何為使用者註冊新的次要驗證條件:

  1. 重新驗證使用者。

  2. 請使用者輸入電話號碼。

  3. 為使用者取得多重要素工作階段:

    Swift

    authResult.user.multiFactor.getSessionWithCompletion() { (session, error) in
      // ...
    }
    

    目標-C

    [authResult.user.multiFactor
      getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                NSError * _Nullable error) {
        // ...
    }];
    
  4. 傳送驗證訊息至使用者的手機。請確認電話號碼的格式開頭是 +,且不得包含其他標點符號或空白字元 (例如:+15105551234)

    Swift

    // Send SMS verification code.
    PhoneAuthProvider.provider().verifyPhoneNumber(
      phoneNumber,
      uiDelegate: nil,
      multiFactorSession: session) { (verificationId, error) in
        // verificationId will be needed for enrollment completion.
    }
    

    目標-C

    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider verifyPhoneNumber:phoneNumber
                                          UIDelegate:nil
                                  multiFactorSession:session
                                          completion:^(NSString * _Nullable verificationID,
                                                        NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.
    }];
    

    雖然並非必要,但最佳做法是事先告知使用者他們將收到簡訊,且須支付一般費率。

    verifyPhoneNumber() 方法會使用無訊息推播通知,在背景啟動應用程式驗證程序。如果無法使用無聲推播通知,系統會改為發出 reCAPTCHA 驗證問題。

  5. 讓系統傳送簡訊驗證碼後,要求使用者驗證驗證碼。然後使用其回應建構 PhoneAuthCredential

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId,
      verificationCode: verificationCode)
    

    目標-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential = [FIRPhoneAuthProvider.provider
                                           credentialWithVerificationID:verificationID
                                           verificationCode:kPhoneSecondFactorVerificationCode];
    
  6. 初始化斷言物件:

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    目標-C

    FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
    
  7. 完成註冊程序。您也可以選擇為第二個因素指定顯示名稱。由於電話號碼在驗證流程期間會遮蓋 (例如 +1******1234),因此這對具有多個次要驗證者的使用者來說非常實用。

    Swift

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
    

    目標-C

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    [authResult.user.multiFactor enrollWithAssertion:assertion
                                         displayName:nil
                                          completion:^(NSError * _Nullable error) {
        // ...
    }];
    

以下程式碼顯示第二重驗證註冊的完整範例:

Swift

let user = Auth.auth().currentUser
user?.multiFactor.getSessionWithCompletion({ (session, error) in
  // Send SMS verification code.
  PhoneAuthProvider.provider().verifyPhoneNumber(
    phoneNumber,
    uiDelegate: nil,
    multiFactorSession: session
  ) { (verificationId, error) in
    // verificationId will be needed for enrollment completion.
    // Ask user for the verification code.
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: phoneSecondFactorVerificationCode)
    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user?.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
  }
})

目標-C

FIRUser *user = FIRAuth.auth.currentUser;
[user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                              NSError * _Nullable error) {
    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumber:phoneNumber
      UIDelegate:nil
      multiFactorSession:session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.

        // Ask user for the verification code.
        // ...

        // Then:
        FIRPhoneAuthCredential *credential =
            [FIRPhoneAuthProvider.provider credentialWithVerificationID:verificationID
                                                        verificationCode:kPhoneSecondFactorVerificationCode];
        FIRMultiFactorAssertion *assertion =
            [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

        // Complete enrollment. This will update the underlying tokens
        // and trigger ID token change listener.
        [user.multiFactor enrollWithAssertion:assertion
                                  displayName:displayName
                                    completion:^(NSError * _Nullable error) {
            // ...
        }];
    }];
}];

恭喜!您已成功為使用者註冊第二重驗證。

透過次要驗證方式登入使用者

如何透過雙重簡訊驗證功能登入使用者帳戶:

  1. 使用第一個因素登入使用者,然後擷取需要多重驗證的錯誤。這項錯誤包含解析器、註冊雙重驗證提示,以及用來證明使用者已透過第一個因素成功驗證的基礎工作階段。

    舉例來說,如果使用者的第一個驗證方法是電子郵件地址和密碼:

    Swift

    Auth.auth().signIn(
      withEmail: email,
      password: password
    ) { (result, error) in
      let authError = error as NSError
      if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
        // The user is a multi-factor user. Second factor challenge is required.
        let resolver =
          authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
        // ...
      } else {
        // Handle other errors such as wrong password.
      }
    }
    

    目標-C

    [FIRAuth.auth signInWithEmail:email
                         password:password
                       completion:^(FIRAuthDataResult * _Nullable authResult,
                                    NSError * _Nullable error) {
        if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
            // User is not enrolled with a second factor and is successfully signed in.
            // ...
        } else {
            // The user is a multi-factor user. Second factor challenge is required.
        }
    }];
    

    如果使用者的第一個因素是 OAuth 等聯合提供者,請在呼叫 getCredentialWith() 後擷取錯誤。

  2. 如果使用者已註冊多個次要因子,請詢問他們要使用的次要因數。您可以使用 resolver.hints[selectedIndex].phoneNumber 取得遮蓋的電話號碼,並使用 resolver.hints[selectedIndex].displayName 取得顯示名稱。

    Swift

    // Ask user which second factor to use. Then:
    if resolver.hints[selectedIndex].factorID == PhoneMultiFactorID {
      // User selected a phone second factor.
      // ...
    } else if resolver.hints[selectedIndex].factorID == TotpMultiFactorID {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    

    目標-C

    FIRMultiFactorResolver *resolver =
        (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
    
    // Ask user which second factor to use. Then:
    FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];
    if (hint.factorID == FIRPhoneMultiFactorID) {
      // User selected a phone second factor.
      // ...
    } else if (hint.factorID == FIRTOTPMultiFactorID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 傳送驗證訊息到使用者的手機:

    Swift

    // Send SMS verification code.
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      // verificationId will be needed for sign-in completion.
    }
    

    目標-C

    // Send SMS verification code
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumberWithMultiFactorInfo:hint
      UIDelegate:nil
      multiFactorSession:resolver.session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        if (error != nil) {
            // Failed to verify phone number.
        }
    }];
    
  4. 簡訊驗證碼發送後,要求使用者驗證代碼,並使用該代碼建構 PhoneAuthCredential

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: verificationCodeFromUser)
    

    目標-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential =
        [FIRPhoneAuthProvider.provider
          credentialWithVerificationID:verificationID
                      verificationCode:verificationCodeFromUser];
    
  5. 使用憑證初始化斷言物件:

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    目標-C

    FIRMultiFactorAssertion *assertion =
        [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
    
  6. 解決登入問題。接著,您可以存取原始登入結果,包括標準供應商專屬資料和驗證憑證:

    Swift

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(with: assertion) { (authResult, error) in
      // authResult will also contain the user, additionalUserInfo, optional
      // credential (null for email/password) associated with the first factor sign-in.
    
      // For example, if the user signed in with Google as a first factor,
      // authResult.additionalUserInfo will contain data related to Google provider that
      // the user signed in with.
    
      // user.credential contains the Google OAuth credential.
      // user.credential.accessToken contains the Google OAuth access token.
      // user.credential.idToken contains the Google OAuth ID token.
    }
    

    目標-C

    // Complete sign-in.
    [resolver resolveSignInWithAssertion:assertion
                              completion:^(FIRAuthDataResult * _Nullable authResult,
                                            NSError * _Nullable error) {
        if (error != nil) {
            // User successfully signed in with the second factor phone number.
        }
    }];
    

以下程式碼是多因素使用者登入的完整範例:

Swift

Auth.auth().signIn(
  withEmail: email,
  password: password
) { (result, error) in
  let authError = error as NSError?
  if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
    let resolver =
      authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver

    // Ask user which second factor to use.
    // ...

    // Then:
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo

    // Send SMS verification code
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      if error != nil {
        // Failed to verify phone number.
      }
      // Ask user for the SMS verification code.
      // ...

      // Then:
      let credential = PhoneAuthProvider.provider().credential(
        withVerificationID: verificationId!,
        verificationCode: verificationCodeFromUser)
      let assertion = PhoneMultiFactorGenerator.assertion(with: credential)

      // Complete sign-in.
      resolver.resolveSignIn(with: assertion) { (authResult, error) in
        if error != nil {
          // User successfully signed in with the second factor phone number.
        }
      }
    }
  }
}

目標-C

[FIRAuth.auth signInWithEmail:email
                     password:password
                   completion:^(FIRAuthDataResult * _Nullable authResult,
                               NSError * _Nullable error) {
    if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
        // User is not enrolled with a second factor and is successfully signed in.
        // ...
    } else {
        FIRMultiFactorResolver *resolver =
            (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];

        // Ask user which second factor to use.
        // ...

        // Then:
        FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];

        // Send SMS verification code
        [FIRPhoneAuthProvider.provider
          verifyPhoneNumberWithMultiFactorInfo:hint
                                    UIDelegate:nil
                            multiFactorSession:resolver.session
                                    completion:^(NSString * _Nullable verificationID,
                                                NSError * _Nullable error) {
            if (error != nil) {
                // Failed to verify phone number.
            }

            // Ask user for the SMS verification code.
            // ...

            // Then:
            FIRPhoneAuthCredential *credential =
                [FIRPhoneAuthProvider.provider
                  credentialWithVerificationID:verificationID
                              verificationCode:kPhoneSecondFactorVerificationCode];
            FIRMultiFactorAssertion *assertion =
                [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

            // Complete sign-in.
            [resolver resolveSignInWithAssertion:assertion
                                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                                    NSError * _Nullable error) {
                if (error != nil) {
                    // User successfully signed in with the second factor phone number.
                }
            }];
        }];
    }
}];

恭喜!您已成功透過多重驗證機制,成功登入使用者。

後續步驟