הפעלת אימות רב-שלבי (MFA) באמצעות TOTP באפליקציה
במאמר הזה מוסבר איך להוסיף אימות רב-שלבי (MFA) באמצעות סיסמה חד-פעמית (OTP) מבוססת-זמן לאפליקציה.
ב-Identity Platform אפשר להשתמש ב-TOTP כשלב נוסף באימות רב-שלבי (MFA). כשמפעילים את התכונה הזו, משתמשים שמנסים להיכנס לאפליקציה רואים בקשה להזנת קוד TOTP. כדי ליצור אותו, הם צריכים להשתמש באפליקציה לאימות חשבונות שיכולה ליצור קודי TOTP תקינים, כמו מאמת החשבונות של Google.
לפני שמתחילים
מפעילים לפחות ספק אחד שתומך באימות רב-שלבי. שימו לב שכל הספקים חוץ מהספקים הבאים תומכים באימות דו-שלבי:
- אימות באמצעות הטלפון
- אימות אנונימי
- טוקנים מותאמים אישית לאימות
- Apple Game Center
מוודאים שהאפליקציה מאמתת את כתובות האימייל של המשתמשים. כדי להשתמש באימות דו-שלבי, צריך לאמת את כתובת האימייל. כך אפשר למנוע מגורמים זדוניים להירשם לשירות עם כתובת אימייל שלא בבעלותם, ואז לנעול את הגישה לבעלים האמיתי של כתובת האימייל על ידי הוספת אמצעי אימות נוסף.
מוודאים שיש לכם את הגרסה הנכונה של הפלטפורמה. אימות רב-שלבי באמצעות TOTP נתמך רק בגרסאות ה-SDK הבאות:
פלטפורמה גרסאות Web SDK גרסה 9.19.1 ואילך Android SDK 22.1.0+ iOS SDK 10.12.0+
הפעלת אימות רב-שלבי (MFA) באמצעות TOTP ברמת הפרויקט
כדי להפעיל TOTP כגורם אימות שני, משתמשים ב-SDK לאדמינים או קוראים לנקודת הקצה של REST בהגדרות הפרויקט.
כדי להשתמש ב-SDK לאדמינים:
אם עדיין לא עשיתם זאת, מתקינים את Firebase Admin Node.js SDK.
אימות רב-שלבי באמצעות TOTP נתמך רק בגרסאות 11.6.0 ואילך של Firebase Admin Node.js SDK.
מריצים את הפקודה הבאה:
import { getAuth } from 'firebase-admin/auth'; getAuth().projectConfigManager().updateProjectConfig( { multiFactorConfig: { providerConfigs: [{ state: "ENABLED", totpProviderConfig: { adjacentIntervals: NUM_ADJ_INTERVALS } }] } })מחליפים את מה שכתוב בשדות הבאים:
NUM_ADJ_INTERVALS: מספר המרווחים הסמוכים של חלונות הזמן שמהם יתקבלו סיסמאות TOTP, מאפס עד עשר. ערך ברירת המחדל הוא 5.סיסמאות חד-פעמיות מבוססות-זמן פועלות כך שכאשר שני צדדים (המאמת והבודק) יוצרים סיסמאות חד-פעמיות באותו חלון זמן (בדרך כלל 30 שניות), הם יוצרים את אותה סיסמה. עם זאת, כדי להתחשב בסטייה בשעון בין הצדדים ובזמן התגובה של בני אדם, אפשר להגדיר את שירות ה-TOTP כך שיקבל גם קודי TOTP מחלונות סמוכים.
כדי להפעיל אימות רב-שלבי באמצעות TOTP באמצעות API בארכיטקטורת REST, מריצים את הפקודה הבאה:
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: מזהה הפרויקט.
NUM_ADJ_INTERVALS: מספר המרווחים של חלון הזמן, מאפס עד עשר. ברירת המחדל היא חמש.סיסמאות חד-פעמיות מבוססות-זמן פועלות כך שכאשר שני צדדים (המאמת והבודק) יוצרים סיסמאות חד-פעמיות באותו חלון זמן (בדרך כלל 30 שניות), הם יוצרים את אותה סיסמה. עם זאת, כדי להתחשב בסטייה בשעון בין הצדדים ובזמן התגובה של בני אדם, אפשר להגדיר את שירות ה-TOTP כך שיקבל גם קודי TOTP מחלונות סמוכים.
הפעלת אימות רב-שלבי באמצעות TOTP ברמת הדייר
כדי להפעיל TOTP כגורם שני לאימות רב-שלבי ברמת הדייר, משתמשים בקוד הבא:
getAuth().tenantManager().updateTenant(TENANT_ID,
{
multiFactorConfig: {
state: 'ENABLED',
providerConfigs: [{
totpProviderConfig: {
adjacentIntervals: NUM_ADJ_INTERVALS
}
}]
}
})
מחליפים את מה שכתוב בשדות הבאים:
-
TENANT_ID: מזהה הדייר כמחרוזת. -
NUM_ADJ_INTERVALS: מספר המרווחים של חלון הזמן, מאפס עד עשר. ברירת המחדל היא חמש.
כדי להפעיל אימות רב-שלבי (MFA) באמצעות TOTP ברמת הדייר (tenant) באמצעות API בארכיטקטורת REST, מריצים את הפקודה הבאה:
curl -X PATCH "https://identitytoolkit.googleapis.com/v2/projects/PROJECT_ID/tenants/TENANT_ID?updateMask=mfaConfig" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-H "X-Goog-User-Project: PROJECT_ID" \
-d \
'{
"mfaConfig": {
"providerConfigs": [{
"totpProviderConfig": {
"adjacentIntervals": NUM_ADJ_INTERVALS
}
}]
}
}'
מחליפים את מה שכתוב בשדות הבאים:
-
PROJECT_ID: מזהה הפרויקט. -
TENANT_ID: מזהה הדייר. -
NUM_ADJ_INTERVALS: מספר המרווחים של חלון הזמן, מאפס עד עשר. ברירת המחדל היא חמש.
בחירת תבנית הרשמה
אתם יכולים לבחור אם האפליקציה שלכם דורשת אימות רב-שלבי, ואיך ומתי לרשום את המשתמשים. אלה כמה דוגמאות נפוצות:
רושמים את הגורם השני לאימות של המשתמש כחלק מההרשמה. כדאי להשתמש בשיטה הזו אם האפליקציה שלכם דורשת אימות רב-שלבי מכל המשתמשים.
להציע אפשרות דילוג על הוספת אמצעי אימות נוסף במהלך ההרשמה. אם אתם רוצים לעודד שימוש באימות רב-שלבי באפליקציה שלכם, אבל לא לחייב אותו, תוכלו להשתמש בגישה הזו.
אפשרות להוסיף אימות דו-שלבי מדף ניהול החשבון או הפרופיל של המשתמש, במקום ממסך ההרשמה. כך מצמצמים את החיכוך במהלך תהליך ההרשמה, ועדיין מאפשרים אימות רב-שלבי למשתמשים שחשובה להם האבטחה.
הוספה הדרגתית של אמצעי אימות נוסף כשמשתמש רוצה לגשת לתכונות עם דרישות אבטחה מוגברות.
רישום משתמשים לאימות רב-שלבי באמצעות TOTP
אחרי שמפעילים אימות דו-שלבי באמצעות TOTP כגורם שני לאימות באפליקציה, צריך להטמיע לוגיקה בצד הלקוח כדי לרשום משתמשים לאימות דו-שלבי באמצעות TOTP:
אינטרנט
import {
multiFactor,
TotpMultiFactorGenerator,
TotpSecret
} from "firebase/auth";
multiFactorSession = await multiFactor(activeUser()).getSession();
totpSecret = await TotpMultiFactorGenerator.generateSecret(
multiFactorSession
);
// Display this URL as a QR code.
const url = totpSecret.generateQrCodeUrl( <user account id> , <app name> );
// Ask the user for the verification code from the OTP app by scanning the QR
// code.
const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment(
totpSecret,
verificationCode
);
// Finalize the enrollment.
return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
Java
user.getMultiFactor().getSession()
.addOnCompleteListener(
new OnCompleteListener<MultiFactorSession>() {
@Override
public void onComplete(@NonNull Task<MultiFactorSession> task) {
if (task.isSuccessful()) {
// Get a multi-factor session for the user.
MultiFactorSession multiFactorSession = task.getResult();
TotpMultiFactorGenerator.generateSecret(multiFactorSession)
.addOnCompleteListener(
new OnCompleteListener<TotpSecret>() {
@Override
public void onComplete(@NonNull Task<TotpSecret> task){
if (task.isSuccessful()) {
TotpSecret secret = task.getResult();
// Display this URL as a QR code for the user to scan.
String qrCodeUrl = secret.generateQrCodeUrl();
// Display the QR code
// ...
// Alternatively, you can automatically load the QR code
// into a TOTP authenticator app with either default or
// specified fallback URL and activity.
// Default fallback URL and activity.
secret.openInOtpApp(qrCodeUrl);
// Specified fallback URL and activity.
// secret.openInOtpApp(qrCodeUrl, fallbackUrl, activity);
}
}
});
}
}
});
// Ask the user for the one-time password (otp) from the TOTP authenticator app.
MultiFactorAssertion multiFactorAssertion =
TotpMultiFactorGenerator.getAssertionForEnrollment(
secret, otp);
// Complete the enrollment.
user
.getMultiFactor()
.enroll(multiFactorAssertion, /* displayName= */ "My TOTP second factor")
.addOnCompleteListener(
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
showToast("Successfully enrolled TOTP second factor!");
setResult(Activity.RESULT_OK);
finish();
}
}
});
Kotlin+KTX
user.multiFactor.session.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Get a multi-factor session for the user.
val session: MultiFactorSession = task.result
val secret: TotpSecret = TotpMultiFactorGenerator.generateSecret(session)
// Display this URL as a QR code for the user to scan.
val qrCodeUrl = secret.generateQrCodeUrl()
// Display the QR code
// ...
// Alternatively, you can automatically load the QR code
// into a TOTP authenticator app with either default or
// specified fallback URL and activity.
// Default fallback URL and activity.
secret.openInOtpApp(qrCodeUrl)
// Specify a fallback URL and activity.
// secret.openInOtpApp(qrCodeUrl, fallbackUrl, activity);
}
}
// Ask the user for the one-time password (otp) from the TOTP authenticator app.
val multiFactorAssertion =
TotpMultiFactorGenerator.getAssertionForEnrollment(secret, otp)
// Complete enrollment.
user.multiFactor.enroll(multiFactorAssertion, /* displayName= */ "My TOTP second factor")
.addOnCompleteListener {
// ...
}
Swift
let user = Auth.auth().currentUser
// Get a multi-factor session for the user
user?.multiFactor.getSessionWithCompletion({ (session, error) in
TOTPMultiFactorGenerator.generateSecret(with: session!) {
(secret, error) in
let accountName = user?.email;
let issuer = Auth.auth().app?.name;
// Generate a QR code
let qrCodeUrl = secret?.generateQRCodeURL(withAccountName: accountName!, issuer: issuer!)
// Display the QR code
// ...
// Alternatively, you can automatically load the QR code
// into a TOTP authenticator app with default fallback UR
// and activity.
secret?.openInOTPAppWithQRCodeURL(qrCodeUrl!);
// Ask the user for the verification code after scanning
let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(with: secret, oneTimePassword: onetimePassword)
// Complete the enrollment
user?.multiFactor.enroll(with: assertion, displayName: accountName) { (error) in
// ...
}
}
})
Objective-C
FIRUser *user = FIRAuth.auth.currentUser;
// Get a multi-factor session for the user
[user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error) {
// ...
[FIRTOTPMultiFactorGenerator generateSecretWithMultiFactorSession:session completion:^(FIRTOTPSecret *_Nullable secret, NSError *_Nullable error) {
NSString *accountName = user.email;
NSString *issuer = FIRAuth.auth.app.name;
// Generate a QR code
NSString *qrCodeUrl = [secret generateQRCodeURLWithAccountName:accountName issuer:issuer];
// Display the QR code
// ...
// Alternatively, you can automatically load the QR code
// into a TOTP authenticator app with default fallback URL
// and activity.
[secret openInOTPAppWithQRCodeURL:qrCodeUrl];
// Ask the user for the verification code after scanning
FIRTOTPMultiFactorAssertion *assertion = [FIRTOTPMultiFactorGenerator assertionForEnrollmentWithSecret:secret oneTimePassword:oneTimePassword];
// Complete the enrollment
[user.multiFactor enrollWithAssertion:assertion
displayName:displayName
completion:^(NSError *_Nullable error) {
// ...
}];
}];
}];
כניסה של משתמשים עם גורם שני
כדי להיכנס עם משתמשים באמצעות TOTP MFA, משתמשים בקוד הבא:
אינטרנט
import {
getAuth,
getMultiFactorResolver,
TotpMultiFactorGenerator,
PhoneMultiFactorGenerator,
signInWithEmailAndPassword
} from "firebase/auth";
const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
.then(function(userCredential) {
// The 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 the user which second factor to use.
if (resolver.hints[selectedIndex].factorId ===
TotpMultiFactorGenerator.FACTOR_ID) {
// Ask the user for the OTP code from the TOTP app.
const multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn(resolver.hints[selectedIndex].uid, otp);
// Finalize the sign-in.
return resolver.resolveSignIn(multiFactorAssertion).then(function(userCredential) {
// The user successfully signed in with the TOTP second factor.
});
} else if (resolver.hints[selectedIndex].factorId ===
PhoneMultiFactorGenerator.FACTOR_ID) {
// Handle the phone MFA.
} else {
// The second factor is unsupported.
}
}
// Handle other errors, such as a wrong password.
else if (error.code == 'auth/wrong-password') {
//...
}
});
Java
FirebaseAuth.getInstance()
.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(
new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// The user is not enrolled with a second factor and is
// successfully signed in.
// ...
return;
}
if (task.getException() instanceof FirebaseAuthMultiFactorException) {
// The user is a multi-factor user. Second factor challenge is required.
FirebaseAuthMultiFactorException error =
(FirebaseAuthMultiFactorException) task.getException();
MultiFactorResolver multiFactorResolver = error.getResolver();
// Display the list of enrolled second factors, user picks one (selectedIndex) from the list.
MultiFactorInfo selectedHint = multiFactorResolver.getHints().get(selectedIndex);
if (selectedHint.getFactorId().equals(TotpMultiFactorGenerator.FACTOR_ID)) {
// Ask the user for the one-time password (otp) from the TOTP app.
// Initialize a MultiFactorAssertion object with the one-time password and enrollment id.
MultiFactorAssertion multiFactorAssertion =
TotpMultiFactorGenerator.getAssertionForSignIn(selectedHint.getUid(), otp);
// Complete sign-in.
multiFactorResolver
.resolveSignIn(multiFactorAssertion)
.addOnCompleteListener(
new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// User successfully signed in with the
// TOTP second factor.
}
}
});
} else if (selectedHint.getFactorId().equals(PhoneMultiFactorGenerator.FACTOR_ID)) {
// Handle Phone MFA.
} else {
// Unsupported second factor.
}
} else {
// Handle other errors such as wrong password.
}
}
});
Kotlin+KTX
FirebaseAuth.getInstance
.signInWithEmailAndPassword(email, password)
.addOnCompleteListener{ task ->
if (task.isSuccessful) {
// User is not enrolled with a second factor and is successfully
// signed in.
// ...
}
if (task.exception is FirebaseAuthMultiFactorException) {
// The user is a multi-factor user. Second factor challenge is
// required.
val multiFactorResolver:MultiFactorResolver =
(task.exception as FirebaseAuthMultiFactorException).resolver
// Display the list of enrolled second factors, user picks one (selectedIndex) from the list.
val selectedHint: MultiFactorInfo = multiFactorResolver.hints[selectedIndex]
if (selectedHint.factorId == TotpMultiFactorGenerator.FACTOR_ID) {
val multiFactorAssertion =
TotpMultiFactorGenerator.getAssertionForSignIn(selectedHint.uid, otp)
multiFactorResolver.resolveSignIn(multiFactorAssertion)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// User successfully signed in with the
// TOTP second factor.
}
// ...
}
} else if (selectedHint.factor == PhoneMultiFactorGenerator.FACTOR_ID) {
// Handle Phone MFA.
} else {
// Invalid MFA option.
}
} else {
// Handle other errors, such as wrong password.
}
}
Swift
Auth.auth().signIn(withEmail: email, password: password) {
(result, error) in
if (error != nil) {
let authError = error! as NSError
if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
let resolver = authError.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
if resolver.hints[selectedIndex].factorID == TOTPMultiFactorID {
let assertion = TOTPMultiFactorGenerator.assertionForSignIn(withEnrollmentID: resolver.hints[selectedIndex].uid, oneTimePassword: oneTimePassword)
resolver.resolveSignIn(with: assertion) {
(authResult, error) in
if (error != nil) {
// User successfully signed in with second factor TOTP.
}
}
} else if (resolver.hints[selectedIndex].factorID == PhoneMultiFactorID) {
// User selected a phone second factor.
// ...
} else {
// Unsupported second factor.
// Note that only phone and TOTP second factors are currently supported.
// ...
}
}
}
else {
// The user is not enrolled with a second factor and is
// successfully signed in.
// ...
}
}
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.
[self signInWithMfaWithError:error];
}
}];
- (void)signInWithMfaWithError:(NSError * _Nullable)error{
FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
// Ask user which second factor to use. Then:
FIRMultiFactorInfo *hint = (FIRMultiFactorInfo *) resolver.hints[selectedIndex];
if (hint.factorID == FIRTOTPMultiFactorID) {
// User selected a totp second factor.
// Ask user for verification code.
FIRMultiFactorAssertion *assertion = [FIRTOTPMultiFactorGenerator assertionForSignInWithEnrollmentID:hint.UID oneTimePassword:oneTimePassword];
[resolver resolveSignInWithAssertion:assertion
completion:^(FIRAuthDataResult *_Nullable authResult,
NSError *_Nullable error) {
if (error != nil) {
// User successfully signed in with the second factor TOTP.
}
}];
} else if (hint.factorID == FIRPhoneMultiFactorID) {
// User selected a phone second factor.
// ...
}
else {
// Unsupported second factor.
// Note that only phone and totp second factors are currently supported.
}
}
בדוגמה שלמעלה, כתובת האימייל והסיסמה משמשים כגורם הראשון.
ביטול ההרשמה לאימות רב-שלבי באמצעות TOTP
בקטע הזה מוסבר איך לטפל במשתמש שמבטל את ההרשמה שלו לאימות רב-שלבי באמצעות TOTP.
אם משתמש נרשם לכמה אפשרויות של אימות דו-שלבי, ואם הוא מבטל את ההרשמה לאפשרות שהופעלה לאחרונה, הוא מקבל הודעה על כך ב-auth/user-token-expired ומתבצעת יציאה מהחשבון. המשתמש צריך להיכנס שוב ולבדוק את פרטי הכניסה הקיימים שלו – למשל, כתובת אימייל וסיסמה.
כדי לבטל את ההרשמה של המשתמש, לטפל בשגיאה ולהפעיל מחדש את האימות, משתמשים בקוד הבא:
אינטרנט
import {
EmailAuthProvider,
TotpMultiFactorGenerator,
getAuth,
multiFactor,
reauthenticateWithCredential,
} from "firebase/auth";
try {
// Unenroll from TOTP MFA.
await multiFactor(currentUser).unenroll(mfaEnrollmentId);
} catch (error) {
if (error.code === 'auth/user-token-expired') {
// If the user was signed out, re-authenticate them.
// For example, if they signed in with a password, prompt them to
// provide it again, then call `reauthenticateWithCredential()` as shown
// below.
const credential = EmailAuthProvider.credential(email, password);
await reauthenticateWithCredential(
currentUser,
credential
);
}
}
Java
List<MultiFactorInfo> multiFactorInfoList = user.getMultiFactor().getEnrolledFactors();
// Select the second factor to unenroll
user
.getMultiFactor()
.unenroll(selectedMultiFactorInfo)
.addOnCompleteListener(
new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
// User successfully unenrolled the selected second factor.
}
else {
if (task.getException() instanceof FirebaseAuthInvalidUserException) {
// Handle reauthentication
}
}
}
});
Kotlin+KTX
val multiFactorInfoList = user.multiFactor.enrolledFactors
// Select the option to unenroll
user.multiFactor.unenroll(selectedMultiFactorInfo)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// User successfully unenrolled the selected second factor.
}
else {
if (task.exception is FirebaseAuthInvalidUserException) {
// Handle reauthentication
}
}
}
Swift
user?.multiFactor.unenroll(with: (user?.multiFactor.enrolledFactors[selectedIndex])!,
completion: { (error) in
if (error.code == AuthErrorCode.userTokenExpired.rawValue) {
// Handle reauthentication
}
})
Objective-C
FIRMultiFactorInfo *unenrolledFactorInfo;
for (FIRMultiFactorInfo *enrolledFactorInfo in FIRAuth.auth.currentUser.multiFactor.enrolledFactors) {
// Pick one of the enrolled factors to delete.
}
[FIRAuth.auth.currentUser.multiFactor unenrollWithInfo:unenrolledFactorInfo
completion:^(NSError * _Nullable error) {
if (error.code == FIRAuthErrorCodeUserTokenExpired) {
// Handle reauthentication
}
}];
המאמרים הבאים
- ניהול משתמשים עם אימות רב-שלבי באמצעות Admin SDK.