アプリの TOTP MFA を有効にする
このドキュメントでは、時間ベースのワンタイム パスワード(TOTP)多要素認証(MFA)をアプリに追加する方法について説明します。
Identity Platform では、MFA の追加要素として TOTP を使用できます。この機能を有効にした場合、ユーザーがアプリにログインしようとすると、アプリに TOTP に対するリクエストが表示されます。有効な TOTP コードを生成するには、Google 認証システムなど、それを生成できる認証システムアプリを使用する必要があります。
準備
MFA をサポートするプロバイダを少なくとも 1 つ有効にします。以下を除くすべてのプロバイダが MFA をサポートしていることにご注意ください。
- 電話認証
- 匿名認証
- カスタム認証トークン
- Apple Game Center
アプリでユーザーのメールアドレスが検証されるようにします。MFA では、メールの確認を行う必要があります。これにより、悪意のある人物が所有していないメールアドレスでサービスに登録してから第 2 要素を追加することで、そのメールアドレスの実際の所有者をロックアウトすることを防ぎます。
プラットフォームのバージョンが正しいことを確認してください。TOTP MFA は、次の SDK バージョンでのみサポートされています。
プラットフォーム バージョン Web SDK v9.19.1+ Android SDK 22.1.0+ iOS SDK 10.12.0+
プロジェクト レベルで TOTP MFA を有効にする
TOTP を第 2 要素として有効にするには、Admin SDK を使用するか、プロジェクト構成 REST エンドポイントを呼び出します。
Admin SDK を使用するには、次の操作を行います。
Firebase Admin Node.js SDK をまだインストールしていない場合は、インストールします。
TOTP MFA は、Firebase Admin Node.js SDK バージョン 11.6.0 以降でのみサポートされています。
以下のコマンドを実行します。
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 は、2 つの当事者(証明者と検証者)が同じ時間枠(通常は 30 秒)内に OTP を生成したときに同じパスワードを生成するようにします。ただし、当事者と他のユーザーの応答時間の間に生じるクロック ドリフトに対応するために、隣接する時間枠の 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 は、2 つの当事者(証明者と検証者)が同じ時間枠(通常は 30 秒)内に OTP を生成したときに同じパスワードを生成するようにします。ただし、当事者と他のユーザーの応答時間の間に生じるクロック ドリフトに対応するために、隣接する時間枠の TOTP も受け入れるように TOTP サービスを構成できます。
テナントレベルで TOTP MFA を有効にする
テナントレベルで MFA の第 2 要素として TOTP を有効にするには、次のコードを使用します。
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 です。
登録パターンを選択する
アプリで多要素認証が必要かどうかと、ユーザーを登録する方法とタイミングを選択できます。一般的なパターンには、次のようなものが含まれます。
登録の一部として、ユーザーの第 2 要素を登録する。アプリがすべてのユーザーに対して多要素認証を必要とする場合は、この方法を使用します。
登録中に第 2 要素の登録をスキップできる選択肢を用意する。アプリで多要素認証を必須とはしないが、推奨する場合は、この方法を使用できます。
登録画面ではなく、ユーザーのアカウントまたはプロフィールの管理ページから第 2 要素を追加する機能を用意する。これにより、登録プロセス中の摩擦が最小限に抑えられる一方、セキュリティに敏感なユーザーは多要素認証を利用できるようになります。
セキュリティ要件が強化された機能にユーザーがアクセスする際には、第 2 要素を段階的に追加することを要求する。
TOTP MFA にユーザーを登録する
アプリの第 2 要素として 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) {
// ...
}];
}];
}];
第 2 要素を使用してユーザー ログインを行う
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 を使用して、プログラムで多要素ユーザーを管理する。