為 iOS 應用程式新增多重驗證機制

本文說明如何為 iOS 應用程式新增簡訊多重驗證機制。

多重驗證可以提升應用程式的安全性。攻擊者通常會竊取密碼及盜用社群媒體帳戶,但攔截簡訊的難度較高。

事前準備

  1. 至少啟用一個支援多重驗證的供應商。除了電話驗證、匿名驗證和 Apple Game Center 以外,所有供應商都支援 MFA。

  2. 請確認您的應用程式會驗證使用者的電子郵件地址。多重驗證需要通過電子郵件驗證。這麼做可防止惡意人士使用不屬於自己的電子郵件地址註冊服務,然後透過新增第二個因素將真正的擁有者鎖在門外。

啟用多重驗證

  1. 前往 Google Cloud 控制台的「Identity Platform MFA」頁面。
    前往多重身分驗證頁面

  2. 在標題為「SMS-Based Multi-Factor Authentication」(簡訊多重驗證) 的方塊中,按一下「Enable」(啟用)。

  3. 輸入您要用來測試應用程式的電話號碼。雖然這不是必要步驟,我們仍強烈建議您註冊測試電話號碼,以免在開發期間發生節流情形。

  4. 如果您尚未授權應用程式的網域,請按一下右側的「新增網域」,將網域加入許可清單。

  5. 按一下 [儲存]

驗證應用程式

Identity Platform 需要驗證簡訊要求是否來自您的應用程式。您可以透過兩種方式進行驗證:

  • 靜默 APN 通知:當您首次為使用者登入時,Identity Platform 可以向使用者的裝置傳送靜默推播通知。如果應用程式收到通知,即可繼續驗證程序。請注意,從 iOS 8.0 開始,您不需要要求使用者允許推播通知使用這項方法。

  • reCAPTCHA 驗證:如果您無法傳送靜默通知 (例如,因為使用者已停用背景重新整理功能,或是您在 iOS 模擬器中測試應用程式),可以使用 reCAPTCHA。在許多情況下,reCAPTCHA 會自動解決問題,無須使用者互動。

使用靜音通知

如要啟用 APN 通知,以便與 Identity Platform 搭配使用,請按照下列步驟操作:

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

  2. 請使用 Firebase 控制台上傳 APN 驗證金鑰 (變更內容會自動轉移至 Google Cloud Identity Platform)。如果您還沒有 APN 驗證金鑰,請參閱「使用 FCM 設定 APN」一文,瞭解如何取得這組金鑰。

    1. 開啟 Firebase 控制台

    2. 前往「專案設定」

    3. 選取「Cloud Messaging」分頁。

    4. 在「APNs 驗證金鑰」下方,點選「iOS 應用程式設定」部分中的「上傳」

    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
      // ...
    }
    

    Objective-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.
    }
    

    Objective-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)
    

    Objective-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)
    

    Objective-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
      // ...
    }
    

    Objective-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
      // ...
    }
  }
})

Objective-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.
      }
    }
    

    Objective-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.
    }
    

    Objective-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.
    }
    

    Objective-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. 傳送 SMS 碼後,請使用者驗證該碼,並用於建構 PhoneAuthCredential

    Swift

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

    Objective-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)
    

    Objective-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.
    }
    

    Objective-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.
        }
      }
    }
  }
}

Objective-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.
                }
            }];
        }];
    }
}];

恭喜!您已成功使用多重驗證登入使用者。

後續步驟