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

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

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

事前準備

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

  2. 啟用您要使用簡訊驗證功能的區域。Firebase 會使用完全封鎖的簡訊區域政策,根據預設,可讓您在更安全的情況下建立專案。

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

使用多用戶群架構

如果您要在多用戶群環境中啟用多重驗證機制,除了這份文件中的其餘操作說明外,您還必須完成下列步驟:

  1. 在 GCP Console 中,選取您要使用的用戶群。

  2. 在程式碼中,將 Auth 執行個體的 tenantId 欄位設為您的用戶群 ID。例如:

    網頁模組 API

    import { getAuth } from "firebase/auth";
    
    const auth = getAuth(app);
    auth.tenantId = "myTenantId1";
    

    網路命名空間 API

    firebase.auth().tenantId = 'myTenantId1';
    

啟用多重驗證功能

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

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

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

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

選擇註冊模式

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

  • 註冊使用者的第二重驗證在註冊過程中。如果應用程式需要所有使用者的多重驗證,請使用這個方法。

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

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

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

設定 reCAPTCHA 驗證器

您必須先設定 reCAPTCHA 驗證器,才能傳送簡訊驗證碼。Firebase 使用 reCAPTCHA 來確保電話號碼驗證要求是來自應用程式允許的網域,以防止濫用情形。

您不需要手動設定 reCAPTCHA 用戶端;用戶端 SDK 的 RecaptchaVerifier 物件會自動建立並初始化任何必要的用戶端金鑰和密鑰。

使用隱形 reCAPTCHA

RecaptchaVerifier 物件支援隱藏的 reCAPTCHA,這種架構通常不需要任何互動即可驗證使用者。如要使用隱藏的 reCAPTCHA,請建立 RecaptchaVerifier,並將 size 參數設為 invisible,並指定用於啟動多重驗證註冊的 UI 元素 ID:

網頁模組 API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier("sign-in-button", {
    "size": "invisible",
    "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
    }
}, auth);

網路命名空間 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
  // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
  onSolvedRecaptcha();
}
});

使用 reCAPTCHA 小工具

如要使用顯示的 reCAPTCHA 小工具,請建立包含小工具的 HTML 元素,然後建立具有 UI 容器 ID 的 RecaptchaVerifier 物件。您也可以選擇設定在 reCAPTCHA 解答或到期時叫用的回呼:

網頁模組 API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier(
    "recaptcha-container",

    // Optional reCAPTCHA parameters.
    {
      "size": "normal",
      "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
      },
      "expired-callback": function() {
        // Response expired. Ask user to solve reCAPTCHA again.
        // ...
      }
    }, auth
);

網路命名空間 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
  'recaptcha-container',
  // Optional reCAPTCHA parameters.
  {
    'size': 'normal',
    'callback': function(response) {
      // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
      // ...
      onSolvedRecaptcha();
    },
    'expired-callback': function() {
      // Response expired. Ask user to solve reCAPTCHA again.
      // ...
    }
  });

預先轉譯 reCAPTCHA

您也可以在啟動雙重驗證之前,視需要預先轉譯 reCAPTCHA:

網頁模組 API

recaptchaVerifier.render()
    .then(function (widgetId) {
        window.recaptchaWidgetId = widgetId;
    });

網路命名空間 API

recaptchaVerifier.render()
  .then(function(widgetId) {
    window.recaptchaWidgetId = widgetId;
  });

render() 解析後,您會取得 reCAPTCHA 小工具 ID,可用來呼叫 reCAPTCHA API

var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);

reCAPTCHA 可透過 verify 方法提取這個邏輯,因此您不需要直接處理 grecaptcha 變數。

註冊第二重驗證方式

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

  1. 重新驗證使用者。

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

  3. 請初始化 reCAPTCHA 驗證器,如上一節所示。如果已設定 RecaptchaVerifier 執行個體,請略過這個步驟:

    網頁模組 API

    import { RecaptchaVerifier } from "firebase/auth";
    
    const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    網路命名空間 API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 為使用者取得多重要素工作階段:

    網頁模組 API

    import { multiFactor } from "firebase/auth";
    
    multiFactor(user).getSession().then(function (multiFactorSession) {
        // ...
    });
    

    網路命名空間 API

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. 使用使用者電話號碼和多重要素工作階段初始化 PhoneInfoOptions 物件:

    網頁模組 API

    // Specify the phone number and pass the MFA session.
    const phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    

    網路命名空間 API

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. 傳送驗證訊息到使用者的手機:

    網頁模組 API

    import { PhoneAuthProvider } from "firebase/auth";
    
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed to complete enrollment.
        });
    

    網路命名空間 API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for enrollment completion.
      })
    

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

  7. 如果要求失敗,請重設 reCAPTCHA,然後重複上一個步驟,讓使用者再試一次。請注意,verifyPhoneNumber() 會在擲回錯誤時自動重設 reCAPTCHA,因為 reCAPTCHA 權杖僅使用一次。

    網頁模組 API

    recaptchaVerifier.clear();
    

    網路命名空間 API

    recaptchaVerifier.clear();
    
  8. 讓系統傳送簡訊驗證碼後,要求使用者驗證驗證碼:

    網頁模組 API

    // Ask user for the verification code. Then:
    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    網路命名空間 API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. 使用 PhoneAuthCredential 初始化 MultiFactorAssertion 物件:

    網頁模組 API

    import { PhoneMultiFactorGenerator } from "firebase/auth";
    
    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    網路命名空間 API

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

    網頁模組 API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
    

    網路命名空間 API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
    

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

網頁模組 API

import {
    multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
    RecaptchaVerifier
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
multiFactor(user).getSession()
    .then(function (multiFactorSession) {
        // Specify the phone number and pass the MFA session.
        const phoneInfoOptions = {
            phoneNumber: phoneNumber,
            session: multiFactorSession
        };

        const phoneAuthProvider = new PhoneAuthProvider(auth);

        // Send SMS verification code.
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
    }).then(function (verificationId) {
        // Ask user for the verification code. Then:
        const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
        const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);

        // Complete enrollment.
        return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
    });

網路命名空間 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
user.multiFactor.getSession().then(function(multiFactorSession) {
  // Specify the phone number and pass the MFA session.
  var phoneInfoOptions = {
    phoneNumber: phoneNumber,
    session: multiFactorSession
  };
  var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
  // Send SMS verification code.
  return phoneAuthProvider.verifyPhoneNumber(
      phoneInfoOptions, recaptchaVerifier);
})
.then(function(verificationId) {
  // Ask user for the verification code.
  var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
  var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
  // Complete enrollment.
  return user.multiFactor.enroll(multiFactorAssertion, mfaDisplayName);
});

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

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

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

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

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

    網頁模組 API

    import { getAuth, getMultiFactorResolver} from "firebase/auth";
    
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email, password)
        .then(function (userCredential) {
            // User successfully signed in and is not enrolled with a second factor.
        })
        .catch(function (error) {
            if (error.code == 'auth/multi-factor-auth-required') {
                // The user is a multi-factor user. Second factor challenge is required.
                resolver = getMultiFactorResolver(auth, error);
                // ...
            } else if (error.code == 'auth/wrong-password') {
                // Handle other errors such as wrong password.
            }
    });
    

    網路命名空間 API

    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(function(userCredential) {
        // User successfully signed in and is not enrolled with a second factor.
      })
      .catch(function(error) {
        if (error.code == 'auth/multi-factor-auth-required') {
          // The user is a multi-factor user. Second factor challenge is required.
          resolver = error.resolver;
          // ...
        } else if (error.code == 'auth/wrong-password') {
          // Handle other errors such as wrong password.
        } ...
      });
    

    如果使用者的第一個要素是聯合提供者 (例如 OAuth、SAML 或 OIDC),請在呼叫 signInWithPopup()signInWithRedirect() 後擷取錯誤。

  2. 如果使用者已註冊多個次要因子,請詢問他們要使用哪一項:

    網頁模組 API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    
    if (resolver.hints[selectedIndex].factorId ===
        PhoneMultiFactorGenerator.FACTOR_ID) {
        // User selected a phone second factor.
        // ...
    } else if (resolver.hints[selectedIndex].factorId ===
               TotpMultiFactorGenerator.FACTOR_ID) {
        // User selected a TOTP second factor.
        // ...
    } else {
        // Unsupported second factor.
    }
    

    網路命名空間 API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    if (resolver.hints[selectedIndex].factorId === firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
      // User selected a phone second factor.
      // ...
    } else if (resolver.hints[selectedIndex].factorId === firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 請初始化 reCAPTCHA 驗證器,如上一節所示。如果已設定 RecaptchaVerifier 執行個體,請略過這個步驟:

    網頁模組 API

    import { RecaptchaVerifier } from "firebase/auth";
    
    recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    網路命名空間 API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 使用使用者電話號碼和多重要素工作階段初始化 PhoneInfoOptions 物件。這些值包含在傳遞至 auth/multi-factor-auth-required 錯誤的 resolver 物件中:

    網頁模組 API

    const phoneInfoOptions = {
        multiFactorHint: resolver.hints[selectedIndex],
        session: resolver.session
    };
    

    網路命名空間 API

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. 傳送驗證訊息到使用者的手機:

    網頁模組 API

    // Send SMS verification code.
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed for sign-in completion.
        });
    

    網路命名空間 API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. 如果要求失敗,請重設 reCAPTCHA,然後重複上一個步驟,讓使用者再試一次:

    網頁模組 API

    recaptchaVerifier.clear();
    

    網路命名空間 API

    recaptchaVerifier.clear();
    
  7. 讓系統傳送簡訊驗證碼後,要求使用者驗證驗證碼:

    網頁模組 API

    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    網路命名空間 API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. 使用 PhoneAuthCredential 初始化 MultiFactorAssertion 物件:

    網頁模組 API

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    網路命名空間 API

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. 請呼叫 resolver.resolveSignIn() 完成次要驗證。接著,您可以存取原始登入結果,包括標準供應商專屬資料和驗證憑證:

    網頁模組 API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
        .then(function (userCredential) {
            // userCredential 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,
            // userCredential.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.
        });
    

    網路命名空間 API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
      .then(function(userCredential) {
        // userCredential 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,
        // userCredential.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.
      });
    

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

網頁模組 API

import {
    getAuth,
    getMultiFactorResolver,
    PhoneAuthProvider,
    PhoneMultiFactorGenerator,
    RecaptchaVerifier,
    signInWithEmailAndPassword
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function (userCredential) {
        // User is not enrolled with a second factor and is successfully
        // signed in.
        // ...
    })
    .catch(function (error) {
        if (error.code == 'auth/multi-factor-auth-required') {
            const resolver = getMultiFactorResolver(auth, error);
            // Ask user which second factor to use.
            if (resolver.hints[selectedIndex].factorId ===
                PhoneMultiFactorGenerator.FACTOR_ID) {
                const phoneInfoOptions = {
                    multiFactorHint: resolver.hints[selectedIndex],
                    session: resolver.session
                };
                const phoneAuthProvider = new PhoneAuthProvider(auth);
                // Send SMS verification code
                return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
                    .then(function (verificationId) {
                        // Ask user for the SMS verification code. Then:
                        const cred = PhoneAuthProvider.credential(
                            verificationId, verificationCode);
                        const multiFactorAssertion =
                            PhoneMultiFactorGenerator.assertion(cred);
                        // Complete sign-in.
                        return resolver.resolveSignIn(multiFactorAssertion)
                    })
                    .then(function (userCredential) {
                        // User successfully signed in with the second factor phone number.
                    });
            } else if (resolver.hints[selectedIndex].factorId ===
                       TotpMultiFactorGenerator.FACTOR_ID) {
                // Handle TOTP MFA.
                // ...
            } else {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

網路命名空間 API

var resolver;
firebase.auth().signInWithEmailAndPassword(email, password)
  .then(function(userCredential) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      resolver = error.resolver;
      // Ask user which second factor to use.
      if (resolver.hints[selectedIndex].factorId ===
          firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
        var phoneInfoOptions = {
          multiFactorHint: resolver.hints[selectedIndex],
          session: resolver.session
        };
        var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
        // Send SMS verification code
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
          .then(function(verificationId) {
            // Ask user for the SMS verification code.
            var cred = firebase.auth.PhoneAuthProvider.credential(
                verificationId, verificationCode);
            var multiFactorAssertion =
                firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
            // Complete sign-in.
            return resolver.resolveSignIn(multiFactorAssertion)
          })
          .then(function(userCredential) {
            // User successfully signed in with the second factor phone number.
          });
      } else if (resolver.hints[selectedIndex].factorId ===
        firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
        // Handle TOTP MFA.
        // ...
      } else {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

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

後續步驟