為網頁應用程式新增多重驗證機制
本文將說明如何在網頁應用程式中新增簡訊多重驗證機制。
多重驗證可以提升應用程式的安全性。攻擊者通常會竊取密碼及盜用社群媒體帳戶,但攔截簡訊的難度較高。
事前準備
至少啟用一個支援多重驗證的供應商。除了電話驗證、匿名驗證和 Apple Game Center 以外,所有供應商都支援 MFA。
啟用您預計使用簡訊驗證的區域。Identity Platform 會採用完全封鎖的簡訊區域政策,讓您在預設情況下以更安全的狀態建立專案。
請確認您的應用程式會驗證使用者的電子郵件地址。多重驗證需要通過電子郵件驗證。這麼做可防止惡意人士使用不屬於自己的電子郵件地址註冊服務,然後透過新增第二個因素將真正的擁有者鎖在門外。
使用多用戶群架構
如果您要啟用多重驗證,以便在多租戶環境中使用,請務必完成下列步驟 (除了本文件的其他操作說明):
在 Google Cloud 控制台中,選取要使用的租用戶。
在程式碼中,將
Auth
例項的tenantId
欄位設為租用戶 ID。例如:網頁版 9
import { getAuth } from "firebase/auth"; const auth = getAuth(app); auth.tenantId = "myTenantId1";
網頁版 8
firebase.auth().tenantId = 'myTenantId1';
啟用多重驗證
前往 Google Cloud 控制台的「Identity Platform MFA」頁面。
前往多重身分驗證頁面在標題為「SMS-Based Multi-Factor Authentication」(簡訊多重驗證) 的方塊中,按一下「Enable」(啟用)。
輸入您要用來測試應用程式的電話號碼。雖然這不是必要步驟,我們仍強烈建議您註冊測試電話號碼,以免在開發期間發生節流情形。
如果您尚未授權應用程式的網域,請按一下右側的「新增網域」,將網域加入許可清單。
按一下 [儲存]。
選擇註冊模式
您可以選擇應用程式是否需要多重驗證,以及註冊使用者的方式和時間。常見的模式包括:
在註冊過程中註冊使用者的第二重驗證。如果您的應用程式要求所有使用者都必須進行多重驗證,請使用這個方法。
提供可略過的選項,讓使用者在註冊時註冊第二重驗證。如要鼓勵使用者採用多重驗證,但不強制要求使用者採用,則可能會偏好這種做法。
提供從使用者帳戶或個人資料管理頁面新增第二因素的功能,而非在註冊畫面中提供。這樣一來,註冊程序的摩擦力會降到最低,同時仍可讓重視安全性的使用者使用多重驗證。
當使用者想要存取需要更高安全性要求的功能時,要求逐步新增第二個因素。
設定 reCAPTCHA 驗證器
您必須先設定 reCAPTCHA 驗證器,才能傳送簡訊驗證碼。Identity Platform 會使用 reCAPTCHA 確保電話號碼驗證要求來自應用程式允許的網域,以防範濫用行為。
您不需要手動設定 reCAPTCHA 用戶端,因為用戶端 SDK 的 RecaptchaVerifier
物件會自動建立及初始化所有必要的用戶端金鑰和密鑰。
使用隱形 reCAPTCHA
RecaptchaVerifier
物件支援隱形 reCAPTCHA,通常不需要使用者互動就能驗證使用者。如要使用隱形 reCAPTCHA,請建立 RecaptchaVerifier
,並將 size
參數設為 invisible
,然後指定啟動多因素註冊的 UI 元素 ID:
網頁版 9
import { RecaptchaVerifier, getAuth } from "firebase/auth";
const recaptchaVerifier = new RecaptchaVerifier(getAuth(), "sign-in-button", {
"size": "invisible",
"callback": function(response) {
// reCAPTCHA solved, you can proceed with
// phoneAuthProvider.verifyPhoneNumber(...).
onSolvedRecaptcha();
}
});
網頁版 8
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 解開或到期時觸發:
網頁版 9
import { RecaptchaVerifier, getAuth } from "firebase/auth";
const recaptchaVerifier = new RecaptchaVerifier(
getAuth(),
"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.
// ...
}
}
);
網頁版 8
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:
網頁版 9
recaptchaVerifier.render()
.then(function (widgetId) {
window.recaptchaWidgetId = widgetId;
});
網頁版 8
recaptchaVerifier.render()
.then(function(widgetId) {
window.recaptchaWidgetId = widgetId;
});
render()
解析完成後,您會取得 reCAPTCHA 的小工具 ID,可用於呼叫 reCAPTCHA API:
var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);
RecaptchaVerifier 會使用 verify 方法抽象化這項邏輯,因此您不必直接處理 grecaptcha
變數。
註冊次要驗證方式
如要為使用者註冊新的次要驗證方法,請按照下列步驟操作:
重新驗證使用者。
請使用者輸入電話號碼。
按照上一節所述,初始化 reCAPTCHA 驗證器。如果已設定 RecaptchaVerifier 例項,請略過這個步驟:
網頁版 9
import { RecaptchaVerifier, getAuth } from "firebase/auth"; const recaptchaVerifier = new RecaptchaVerifier( getAuth(),'recaptcha-container-id', undefined);
網頁版 8
var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
為使用者取得多重驗證工作階段:
網頁版 9
import { multiFactor } from "firebase/auth"; multiFactor(user).getSession().then(function (multiFactorSession) { // ... });
網頁版 8
user.multiFactor.getSession().then(function(multiFactorSession) { // ... })
使用使用者的電話號碼和多重驗證工作階段,初始化
PhoneInfoOptions
物件:網頁版 9
// Specify the phone number and pass the MFA session. const phoneInfoOptions = { phoneNumber: phoneNumber, session: multiFactorSession };
網頁版 8
// Specify the phone number and pass the MFA session. var phoneInfoOptions = { phoneNumber: phoneNumber, session: multiFactorSession };
將驗證訊息傳送至使用者的手機:
網頁版 9
import { PhoneAuthProvider } from "firebase/auth"; const phoneAuthProvider = new PhoneAuthProvider(auth); phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier) .then(function (verificationId) { // verificationId will be needed to complete enrollment. });
網頁版 8
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. })
雖然這不是強制規定,但建議您事先告知使用者,他們將收到簡訊,並且會收取標準費率。
如果要求失敗,請重設 reCAPTCHA,然後重複上一個步驟,讓使用者重新嘗試。請注意,由於 reCAPTCHA 憑證只能使用一次,因此
verifyPhoneNumber()
會在 reCAPTCHA 擲回錯誤時自動重設 reCAPTCHA。網頁版 9
recaptchaVerifier.clear();
網頁版 8
recaptchaVerifier.clear();
傳送簡訊碼後,請要求使用者驗證這組碼:
網頁版 9
// Ask user for the verification code. Then: const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
網頁版 8
// Ask user for the verification code. Then: var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
使用
PhoneAuthCredential
初始化MultiFactorAssertion
物件:網頁版 9
import { PhoneMultiFactorGenerator } from "firebase/auth"; const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
網頁版 8
var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
完成註冊。您可以選擇為第二個因素指定顯示名稱。這對擁有多個次要驗證方法的使用者來說很有幫助,因為電話號碼會在驗證流程中遮蓋 (例如 +1******1234)。
網頁版 9
// Complete enrollment. This will update the underlying tokens // and trigger ID token change listener. multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
網頁版 8
// Complete enrollment. This will update the underlying tokens // and trigger ID token change listener. user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
以下程式碼為註冊第二個因素的完整範例:
網頁版 9
import {
multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
RecaptchaVerifier, getAuth
} from "firebase/auth";
const recaptchaVerifier = new RecaptchaVerifier(getAuth(),
'recaptcha-container-id', undefined);
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);
});
網頁版 8
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);
});
恭喜!您已為使用者成功註冊第二個驗證因素。
使用第二種驗證方法登入
如要使用簡訊雙重驗證登入使用者,請按照下列步驟操作:
請使用者以第一個因素登入,然後擷取
auth/multi-factor-auth-required
錯誤。這個錯誤包含解析器、已註冊的雙重驗證提示,以及證明使用者已成功透過第一個因素驗證的基礎工作階段。舉例來說,如果使用者的第一個因素是電子郵件和密碼:
網頁版 9
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. } });
網頁版 8
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()
後擷取錯誤。如果使用者註冊了多個次要驗證因素,請詢問他們要使用哪一個:
網頁版 9
// 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. }
網頁版 8
// 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. }
按照上一節所述,初始化 reCAPTCHA 驗證器。如果您已設定 RecaptchaVerifier 例項,請略過這個步驟:
網頁版 9
import { RecaptchaVerifier, getAuth } from "firebase/auth"; recaptchaVerifier = new RecaptchaVerifier(getAuth(), 'recaptcha-container-id', undefined);
網頁版 8
var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
使用使用者的電話號碼和多重驗證工作階段,初始化
PhoneInfoOptions
物件。這些值包含在傳遞至auth/multi-factor-auth-required
錯誤的resolver
物件中:網頁版 9
const phoneInfoOptions = { multiFactorHint: resolver.hints[selectedIndex], session: resolver.session };
網頁版 8
var phoneInfoOptions = { multiFactorHint: resolver.hints[selectedIndex], session: resolver.session };
將驗證訊息傳送至使用者的手機:
網頁版 9
// Send SMS verification code. const phoneAuthProvider = new PhoneAuthProvider(auth); phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier) .then(function (verificationId) { // verificationId will be needed for sign-in completion. });
網頁版 8
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. })
如果要求失敗,請重設 reCAPTCHA,然後重複執行上一個步驟,讓使用者重試:
網頁版 9
recaptchaVerifier.clear();
網頁版 8
recaptchaVerifier.clear();
傳送簡訊碼後,請要求使用者驗證這組碼:
網頁版 9
const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
網頁版 8
// Ask user for the verification code. Then: var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
使用
PhoneAuthCredential
初始化MultiFactorAssertion
物件:網頁版 9
const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
網頁版 8
var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
請呼叫
resolver.resolveSignIn()
完成次要驗證。接著,您可以存取原始登入結果,其中包含標準提供者專屬資料和驗證憑證:網頁版 9
// 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. });
網頁版 8
// 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. });
以下程式碼為多重驗證使用者登入的完整範例:
網頁版 9
import {
getAuth,
getMultiFactorResolver,
PhoneAuthProvider,
PhoneMultiFactorGenerator,
RecaptchaVerifier,
signInWithEmailAndPassword
} from "firebase/auth";
const recaptchaVerifier = new RecaptchaVerifier(getAuth(),
'recaptcha-container-id', undefined);
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.
}
});
網頁版 8
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.
} ...
});
恭喜!您已成功使用多重驗證登入使用者。
後續步驟
- 使用 Admin SDK 以程式輔助的方式管理多重驗證使用者。