为应用启用 TOTP MFA
本文档介绍了如何向应用添加基于时间的动态密码 (TOTP) 多重身份验证 (MFA)。
借助 Identity Platform,您可以将 TOTP 用作 MFA 的其他因素。启用此功能后,尝试登录您的应用的用户会看到系统要求提供 TOTP。要生成 TOTP,用户必须使用可生成有效 TOTP 代码的身份验证器应用,例如 Google 身份验证器。
准备工作
请至少启用一个支持 MFA 的提供方。请注意,除下列情况之外的所有提供方都支持 MFA:
- 电话身份验证
- 匿名身份验证
- 自定义身份验证令牌
- Apple 游戏中心
确保您的应用验证用户电子邮件地址。MFA 要求验证电子邮件地址。这样可以防止恶意操作者使用别人的电子邮件地址注册服务,然后通过添加第二重身份验证阻止实际的电子邮件地址所有者注册。
请确保您使用的是正确的平台版本。只有以下 SDK 版本支持 TOTP MFA:
平台 版本 Web SDK v9.19.1+ Android SDK 22.1.0+ iOS SDK 10.12.0+
在项目级启用 TOTP MFA
如需启用 TOTP 作为第二重身份验证,请使用 Admin SDK 或调用项目配置 REST 端点。
如需使用 Admin SDK,请执行以下操作:
如果您尚未安装 Firebase Admin Node.js SDK,请进行安装。
只有 Firebase Admin Node.js SDK 11.6.0 版及更高版本支持 TOTP MFA。
运行以下命令:
import { getAuth } from 'firebase-admin/auth'; getAuth().projectConfigManager().updateProjectConfig( { multiFactorConfig: { providerConfigs: [{ state: "ENABLED", totpProviderConfig: { adjacentIntervals: NUM_ADJ_INTERVALS } }] } })
替换以下内容:
NUM_ADJ_INTERVALS
:接受 TOTP 的相邻时间范围间隔数(从 0 到 10)。默认值为 5。TOTP 的工作原理是确保两方(证明器和验证器)在同一时间范围内(通常为 30 秒)生成动态密码时,两者会生成相同的密码。但是,为了适应各方和人工响应时间之间的时钟偏移,您可以将 TOTP 服务配置为也接受相邻时间范围的 TOTP。
如需使用 REST API 启用 TOTP MFA,请运行以下代码:
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
:项目 ID。NUM_ADJ_INTERVALS
:时间范围间隔数(从 0 到 10)。默认值为 5。TOTP 的工作原理是确保两方(证明器和验证器)在同一时间范围内(通常为 30 秒)生成动态密码时,两者会生成相同的密码。但是,为了适应各方和人工响应时间之间的时钟偏移,您可以将 TOTP 服务配置为也接受相邻时间范围的 TOTP。
在租户级别启用 TOTP MFA
如需在租户级别启用 TOTP 作为 MFA 的第二重身份验证,请使用以下代码:
getAuth().tenantManager().updateTenant(TENANT_ID,
{
multiFactorConfig: {
state: 'ENABLED',
providerConfigs: [{
totpProviderConfig: {
adjacentIntervals: NUM_ADJ_INTERVALS
}
}]
}
})
替换以下内容:
TENANT_ID
:字符串租户 ID。NUM_ADJ_INTERVALS
:时间范围间隔数(从 0 到 10)。默认值为 5。
如需在租户级别使用 REST API 启用 TOTP MFA,请运行以下代码:
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
:项目 ID。TENANT_ID
:租户 ID。NUM_ADJ_INTERVALS
:时间范围间隔数(从 0 到 10)。默认值为 5。
选择注册模式
您可以选择应用是否要求多重身份验证,以及何时和如何注册用户。一些常见模式包括:
在注册过程中注册用户的第二重身份验证。如果应用要求所有用户进行多重身份验证,请使用此方法。
提供可在注册期间跳过第二重身份验证注册的选项。如果您想要建议但不要求在应用中使用多重身份验证,可以使用此方法。
提供从用户的账号或个人资料管理页面(而不是注册界面)添加第二重身份验证的功能。这样可以使注册过程更顺畅,同时仍可为注重安全的用户提供多重身份验证。
如果用户希望访问安全性要求更高的功能,再要求添加第二重身份验证。
在 TOTP MFA 中注册用户
启用 TOTP MFA 作为应用的第二重身份验证后,请实现客户端逻辑以在 TOTP MFA 中注册用户:
Web
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 登录,请使用以下代码:
Web
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 MFA
本部分介绍如何处理用户取消注册 TOTP MFA 的情况。
如果用户注册了多个 MFA 选项,并且用户取消注册了最近启用的选项,则会接收到 auth/user-token-expired
并退出账号。用户必须重新登录并验证其现有凭据,例如电子邮件地址和密码。
如需取消注册用户、处理错误并触发重新身份验证,请使用以下代码:
Web
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 以编程方式管理多重身份验证用户。