與多重驗證使用者合作

本文說明如何為已註冊多重驗證的 Identity Platform 使用者執行常見工作。

更新使用者的電子郵件地址

使用多重驗證的使用者必須隨時提供已驗證的電子郵件地址。這麼做可防止惡意人士使用不屬於自己的電子郵件地址註冊應用程式,然後透過新增第二因素,將真正的擁有者鎖在系統外。

如要更新使用者的電子郵件地址,請使用 verifyBeforeUpdateEmail() 方法。與 updateEmail() 不同,使用這個方法時,使用者必須先點選驗證連結,Identity Platform 才會更新電子郵件地址。例如:

網頁版 8

var user = firebase.auth().currentUser;
user.verifyBeforeUpdateEmail(newEmail).then(function() {
  // Email sent.
  // User must click the email link before the email is updated.
}).catch(function(error) {
  // An error happened.
});

網頁版 9

import { getAuth, verifyBeforeUpdateEmail } from "firebase/auth";

const auth = getAuth(firebaseApp);
verifyBeforeUpdateEmail(auth.currentUser, newEmail).then(() => {
  // Email sent.
  // User must click the email link before the email is updated.
}).catch((error) => {
  // An error happened.
});

iOS

let user = Auth.auth().currentUser
user.verifyBeforeUpdateEmail(newEmail, completion: { (error) in
  if error != nil {
    // An error happened.
  }
  // Email sent.
  // User must click the email link before the email is updated.
})

Android

FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
user.verifyBeforeUpdateEmail(newEmail)
   .addOnCompleteListener(
      new OnCompleteListener<Void>() {
       @Override
       public void onComplete(@NonNull Task<Void> task) {
         if (task.isSuccessful()) {
          // Email sent.
          // User must click the email link before the email is updated.
         } else {
          // An error occurred.
         }
       }
      });

根據預設,Identity Platform 會傳送電子郵件給使用者,並提供簡單的網路處理常式來處理驗證程序。您可以透過幾種方式自訂這項流程。

驗證電子郵件本地化

如要將 Identity Platform 傳送的電子郵件本地化,請在呼叫 verifyBeforeUpdateEmail() 之前設定語言代碼:

網頁版 8

firebase.auth().languageCode = 'fr';

網頁版 9

import { getAuth } from "firebase/auth";

const auth = getAuth();
auth.languageCode = 'fr';

iOS

Auth.auth().languageCode = 'fr';

Android

FirebaseAuth.getInstance().setLanguageCode("fr");

傳遞其他狀態

您可以使用動作碼設定,在驗證電子郵件中加入其他狀態,或透過行動應用程式處理驗證。例如:

網頁版 8

var user = firebase.auth().currentUser;
var actionCodeSettings = {
  url: 'https://www.example.com/completeVerification?state=*****',
  iOS: {
    bundleId: 'com.example.ios'
  },
  android: {
    packageName: 'com.example.android',
    installApp: true,
    minimumVersion: '12'
  },
  handleCodeInApp: true,
  // When multiple custom dynamic link domains are defined, specify which
  // one to use.
  dynamicLinkDomain: "example.page.link"
};
user.verifyBeforeUpdateEmail(newEmail, actionCodeSettings).then(function() {
  // Email sent.
  // User must click the email link before the email is updated.
}).catch(function(error) {
  // An error happened.
});

網頁版 9

import { getAuth, verifyBeforeUpdateEmail } from "firebase/auth";

const auth = getAuth(firebaseApp);
const user = auth.currentUser
const actionCodeSettings = {
  url: 'https://www.example.com/completeVerification?state=*****',
  iOS: {
    bundleId: 'com.example.ios'
  },
  android: {
    packageName: 'com.example.android',
    installApp: true,
    minimumVersion: '12'
  },
  handleCodeInApp: true,
  // When multiple custom dynamic link domains are defined, specify which
  // one to use.
  dynamicLinkDomain: "example.page.link"
};

  verifyBeforeUpdateEmail(auth.currentUser, newEmail, actionCodeSettings).then(() => {
    // Email sent.
    // User must click the email link before the email is updated.
  }).catch((error) => {
    // An error happened.
  })

iOS

var actionCodeSettings = ActionCodeSettings.init()
actionCodeSettings.canHandleInApp = true
let user = Auth.auth().currentUser()
actionCodeSettings.URL =
    String(format: "https://www.example.com/?email=%@", user.email)
actionCodeSettings.iOSbundleID = Bundle.main.bundleIdentifier!
actionCodeSettings.setAndroidPakageName("com.example.android",
                                         installIfNotAvailable:true,
                                         minimumVersion:"12")
// When multiple custom dynamic link domains are defined, specify which one to use.
actionCodeSettings.dynamicLinkDomain = "example.page.link"
user.sendEmailVerification(withActionCodeSettings:actionCodeSettings { error in
  if error != nil {
    // Error occurred. Inspect error.code and handle error.
    return
  }
  // Email verification sent.
})
user.verifyBeforeUpdateEmail(newEmail, actionCodeSettings, completion: { (error) in
  if error != nil {
    // An error happened.
  }
  // Email sent.
  // User must click the email link before the email is updated.
})

Android

ActionCodeSettings actionCodeSettings =
    ActionCodeSettings.newBuilder()
       .setUrl("https://www.example.com/completeVerification?state=*****")
       .setHandleCodeInApp(true)
       .setAndroidPackageName(
         "com.example.android",
         /* installIfNotAvailable= */ true,
         /* minimumVersion= */ null)
       .setIOSBundleId("com.example.ios")
       // When multiple custom dynamic link domains are defined, specify
       // which one to use.
       .setDynamicLinkDomain("example.page.link")
       .build();
FirebaseUser multiFactorUser = FirebaseAuth.getInstance().getCurrentUser();
multiFactorUser
   .verifyBeforeUpdateEmail(newEmail, actionCodeSettings)
   .addOnCompleteListener(
      new OnCompleteListener<Void>() {
       @Override
       public void onComplete(@NonNull Task<Void> task) {
         if (task.isSuccessful()) {
          // Email sent.
          // User must click the email link before the email is updated.
         } else {
          // An error occurred.
         }
       }
      });

自訂驗證處理常式

您可以自行建立處理電子郵件驗證的處理常式。以下範例說明如何在套用動作程式碼前,檢查其中繼資料:

網頁版 8

var email;
firebase.auth().checkActionCode(actionCode)
  .then(function(info) {
    // Operation is equal to
    // firebase.auth.ActionCodeInfo.Operation.VERIFY_AND_CHANGE_EMAIL
    var operation = info['operation'];
    // This is the old email.
    var previousEmail = info['data']['previousEmail'];
    // This is the new email the user is changing to.
    email = info['data']['email'];
    // TODO: Display a message to the end user that the email address of the account is
    // going to be changed from `fromEmail` to `email`
    // 
    // On confirmation.
    return firebase.auth().applyActionCode(actionCode)
  }).then(function() {
    // Confirm to the end user the email was updated.
    showUI('You can now sign in with your new email: ' + email);
  })
  .catch(function(error) {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
  });

網頁版 9

import { getAuth,  checkActionCode, applyActionCode} from "firebase/auth";

const auth = getAuth(firebaseApp);
var email;
checkActionCode(auth, actionCode)
  .then((info) => {
    // Operation is equal to
    // ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL
    const operation = info['operation'];
    // This is the old email.
    const previousEmail = info['data']['previousEmail'];
    // This is the new email the user is changing to.
    email = info['data']['email'];
    // TODO: Display a message to the end user that the email address of the account is
    // going to be changed from `fromEmail` to `email`
    // 
    // On confirmation.
    return applyActionCode(auth, actionCode)
  }).then(() => {
    // Confirm to the end user the email was updated.
    showUI('You can now sign in with your new email: ' + email);
  })
  .catch((error) => {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
  });

iOS

Auth.auth().checkActionCode(actionCode) { info, error in
  if error != nil {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
    return
  }
  // This is the new email the user is changing to.
  let email = info?.email
  // This is the old email.
  let oldEmail = info?.previousEmail
  // operation is equal to
  // firebase.auth.ActionCodeInfo.Operation.VERIFY_AND_CHANGE_EMAIL
  let operation = info?.operation
  // TODO: Display a message to the end user that the email address of the account is
  // going to be changed from `fromEmail` to `email`
  // 
  // On confirmation.
  return Auth.auth().applyActionCode(actionCode)
}

Android

FirebaseAuth.getInstance().checkActionCode(actionCode).addOnCompleteListener(
  new OnCompleteListener<ActionCodeResult>() {
    @Override
    public void onComplete(@NonNull Task<ActionCodeResult> task) {
      if (!task.isSuccessful()) {
        // Error occurred during confirmation. The code might have expired or the
        // link has been used before.
        return;
      }
      ActionCodeResult result = task.getResult();

      // This maps to VERIFY_AND_CHANGE_EMAIL.
      int operation = result.getOperation();

      if (operation == ActionCodeResult.VERIFY_AND_CHANGE_EMAIL) {
        ActionCodeEmailInfo actionCodeInfo =
            (ActionCodeEmailInfo) result.getInfo();

        String fromEmail = actionCodeInfo.getFromEmail();
        String email = actionCodeInfo.getEmail();
        // TODO: Display a message to the user that the email address
        // of the account is changing from `fromEmail` to `email` once
        // they confirm.
      }
    }
  });

如需更多資訊,請參閱 Firebase 說明文件中的「建立自訂電子郵件動作處理常式」一文。

重新驗證使用者

即使使用者已登入,您可能還是想在執行敏感作業 (例如) 前,要求他們重新驗證身分:

  • 變更密碼。
  • 新增或移除新的次要驗證方式。
  • 更新個人資訊 (例如地址)。
  • 執行金融交易。
  • 刪除使用者帳戶。

如要使用電子郵件和密碼重新驗證使用者,請按照下列步驟操作:

網頁版

var resolver;
var credential = firebase.auth.EmailAuthProvider.credential(
    firebase.auth().currentUser.email, password);
firebase.auth().currentUser.reauthenticateWithCredential(credential)
  .then(function(userCredential) {
    // User successfully re-authenticated and does not require a second factor challenge.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      // Handle multi-factor authentication.
    } else {
      // Handle other errors.
    }
  });

iOS

let credential = EmailAuthProvider.credential(withEmail: email, password: password)
Auth.auth().currentUser.reauthenticate(with: credential, completion: { (result, error) in
  let authError = error as NSError?
  if (authError == nil || authError!.code != AuthErrorCode.secondFactorRequired.rawValue) {
    // User is not enrolled with a second factor or is successfully signed in.
  } else {
    // Handle multi-factor authentication.
  }
})

Android

FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
AuthCredential credential = EmailAuthProvider.getCredential(user.getEmail(), password);
user.reauthenticate(credential)
   .addOnCompleteListener(
      new OnCompleteListener<AuthResult>() {
       @Override
       public void onComplete(@NonNull Task<AuthResult> task) {
         if (task.isSuccessful()) {
          // User successfully re-authenticated and does not
          // require a second factor challenge.
          // ...
          return;
         }
         if (task.getException() instanceof FirebaseAuthMultiFactorException) {
           // Handle multi-factor authentication.
         } else {
          // Handle other errors.
         }
       }
      });

如要使用 Microsoft 等 OAuth 供應商重新驗證,請按照下列步驟操作:

網頁版

var resolver;
var user = firebase.auth().currentUser;
// Ask the user to re-authenticate with Microsoft.
var provider = new firebase.auth.OAuthProvider('microsoft.com');
// Microsoft provider allows the ability to provide a login_hint.
provider.setCustomParameters({
  login_hint: user.email
});
user.reauthenticateWithPopup(provider)
  .then(function(userCredential) {
    // User successfully re-authenticated and does not require a second factor challenge.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      // Handle multi-factor authentication.
    } else {
      // Unsupported second factor.
    } else {
      // Handle other errors.
    }
  });

iOS

var provider = OAuthProvider(providerID: "microsoft.com")
  // Replace nil with the custom class that conforms to AuthUIDelegate
  // you created in last step to use a customized web view.
  provider.getCredentialWith(nil) { credential, error in
    Auth.auth().currentUser.reauthenticate(with: credential, completion: { (result, error) in
      let authError = error as NSError?
      if (authError == nil || authError!.code != AuthErrorCode.secondFactorRequired.rawValue) {
        // User is not enrolled with a second factor or is successfully signed in.
        // ...
      } else {
        // Handle multi-factor authentication.
      }
    }
  })

Android

FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
OAuthProvider.Builder provider = OAuthProvider.newBuilder("microsoft.com");
provider.addCustomParameter("login_hint", user.getEmail());
user.startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
   .addOnCompleteListener(
      new OnCompleteListener<AuthResult>() {
       @Override
       public void onComplete(@NonNull Task<AuthResult> task) {
         if (task.isSuccessful()) {
          // User successfully re-authenticated and does not
          // require a second factor challenge.
          // ...
          return;
         }
         if (task.getException() instanceof FirebaseAuthMultiFactorException) {
           // Handle multi-factor authentication.
         } else {
          // Handle other errors such as wrong password.
         }
       }
      });

撤銷最近新增的次要驗證方式

使用者註冊第二因素驗證後,Identity Platform 會傳送電子郵件通知。為防範未經授權的活動,電子郵件會提供取消新增第二因素的選項。

Identity Platform 提供預設的電子郵件範本和處理常式,但您也可以自行建構。以下範例說明如何建立自訂處理程序:

網頁版 8

var obfuscatedPhoneNumber;
firebase.auth().checkActionCode(actionCode)
  .then(function(info) {
    // operation is equal to
    // firebase.auth.ActionCodeInfo.Operation.REVERT_SECOND_FACTOR_ADDITION
    var operation = info['operation'];
    // info.data.multiFactorInfo contains the data corresponding to the
    // enrolled second factor that the user is revoking.
    var multiFactorInfo = info['data']['multiFactorInfo'];
    obfuscatedPhoneNumber = multiFactorInfo['phoneNumber'];
    var displayName = multiFactorInfo['displayName'];
    // TODO: Display a message to the end user about the second factor that
    // was enrolled before the user can confirm the action to revert it.
    // ...
    // On confirmation.
    return firebase.auth().applyActionCode(actionCode)
  }).then(function() {
    // Confirm to the end user the phone number was removed from the account.
    showUI('The phone number ' + obfuscatedPhoneNumber +
         ' has been removed as a second factor from your account.' +
         ' You may also want to reset your password if you suspect' +
         ' your account was compromised.');
  })
  .catch(function(error) {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
  });

網頁版 9

const {
  getAuth,
  checkActionCode,
  applyActionCode
} = require("firebase/auth");

const auth = getAuth(firebaseApp);
var obfuscatedPhoneNumber;
checkActionCode(auth, actionCode)
  .then((info) => {
    // Operation is equal to
    // ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION
    const operation = info['operation'];
    // info.data.multiFactorInfo contains the data corresponding to the
    // enrolled second factor that the user is revoking.
    var multiFactorInfo = info['data']['multiFactorInfo'];
    obfuscatedPhoneNumber = multiFactorInfo['phoneNumber'];
    const displayName = multiFactorInfo['displayName'];
    // TODO: Display a message to the end user about the second factor that
    // was enrolled before the user can confirm the action to revert it.
    // ...
    // On confirmation.
    return applyActionCode(auth, actionCode)
  }).then(() => {
    // Confirm to the end user the phone number was removed from the account.
    showUI('The phone number ' + obfuscatedPhoneNumber +
         ' has been removed as a second factor from your account.' +
         ' You may also want to reset your password if you suspect' +
         ' your account was compromised.');
  })
  .catch((error) => {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
  });

iOS

Auth.auth().checkActionCode(actionCode) { info, error in
  if error != nil {
    // Error occurred during confirmation. The code might have expired or the
    // link has been used before.
    return
  }
  // This is the new email the user is changing to.
  let email = info?.email
  // This is the old email.
  let oldEmail = info?.previousEmail
  // operation is equal to
  // firebase.auth.ActionCodeInfo.Operation.REVERT_SECOND_FACTOR_ADDITION
  let operation = info?.operation
  // info.multiFactorInfo contains the data corresponding to the enrolled second
  // factor that the user is revoking.
  let multiFactorInfo = info?.multiFactorInfo
  let obfuscatedPhoneNumber = (multiFactorInfo as! PhoneMultiFactorInfo).phone
  // TODO: Display a message to the end user that the email address of the account is
  // going to be changed from `fromEmail` to `email`
  // 
  // On confirmation.
  return Auth.auth().applyActionCode(actionCode)
}

Android

FirebaseAuth.getInstance()
  .checkActionCode(actionCode)
  .continueWithTask(
      new Continuation<ActionCodeResult, Task<Void>>() {
        @Override
        public Task<Void> then(Task<ActionCodeResult> task) throws Exception {
          if (!task.isSuccessful()) {
            // Error occurred during confirmation. The code might have expired
            // or the link has been used before.
            return Tasks.forException(task.getException());
          }
          ActionCodeResult result = task.getResult();
          // The operation is equal to ActionCodeResult.REVERT_SECOND_FACTOR_ADDITION.
          int operation = result.getOperation();
          // The ActionCodeMultiFactorInfo contains the data corresponding to
          // the enrolled second factor that the user is revoking.
          ActionCodeMultiFactorInfo actionCodeInfo =
              (ActionCodeMultiFactorInfo) result.getInfo();
          PhoneMultiFactorInfo multiFactorInfo =
              (PhoneMultiFactorInfo) actionCodeInfo.getMultiFactorInfo();
          String obfuscatedPhoneNumber = multiFactorInfo.getPhoneNumber();
          String displayName = multiFactorInfo.getDisplayName();
          // We can now display a message to the end user about the second
          // factor that was enrolled before they confirm the action to revert
          // it.
          // ...
          // On user confirmation:
          return FirebaseAuth.getInstance().applyActionCode(actionCode);
        }
      })
  .addOnCompleteListener(
      new OnCompleteListener<Void>() {
        @Override
        public void onComplete(Task<Void> task) {
          if (task.isSuccessful()) {
            // Display a message to the user that the second factor
            // has been reverted.
          }
        }
      });

如需更多資訊,請參閱 Firebase 說明文件中的「建立自訂電子郵件動作處理常式」一文。

復原次要驗證方式

Identity Platform 並未提供內建機制來復原第二因素。如果使用者無法存取第二因素驗證碼,系統就會將他們鎖定帳戶。如要避免發生這種情況,請考慮下列事項:

  • 警告使用者,如果未設定第二因素驗證,就會失去帳戶存取權。
  • 強烈建議使用者註冊備用次要因素。
  • 使用 Admin SDK 建構復原流程,如果使用者能夠充分驗證身分 (例如上傳復原金鑰或回答個人問題),就會停用多重驗證。
  • 授予支援團隊管理使用者帳戶的權限 (包括移除第二層驗證機制),並提供使用者在帳戶遭鎖定時聯絡支援團隊的選項。

使用者無法透過重設密碼來略過多重驗證。如果您使用 sendPasswordResetEmail() 重設使用者的密碼,他們仍必須在使用新密碼登入時通過多重驗證挑戰。

取消註冊次要驗證方式

如要取消註冊第二個因素,請從使用者已註冊因素的清單中取得該因素,然後呼叫 unenroll()。由於這是敏感作業,如果使用者最近未登入,您必須先重新驗證使用者。

網頁版 8

var options = user.multiFactor.enrolledFactors;
// Ask user to select from the enrolled options.
return user.multiFactor.unenroll(options[selectedIndex])
  .then(function() {
    // User successfully unenrolled selected factor.
  });

網頁版 9

const multiFactorUser = multiFactor(auth.currentUser);
const options = multiFactorUser.enrolledFactors
// Ask user to select from the enrolled options.
return multiFactorUser.unenroll(options[selectedIndex])
  .then(() =>
    // User successfully unenrolled selected factor.
  });

iOS

// Ask user to select from the enrolled options.
user?.multiFactor.unenroll(with: (user?.multiFactor.enrolledFactors[selectedIndex])!,
  completion: { (error) in
    // ...
})

Android

List<MultiFactorInfo> options = user.getMultiFactor().getEnrolledFactors();
// Ask user to select from the enrolled options.
user.getMultiFactor()
   .unenroll(options.get(selectedIndex))
   .addOnCompleteListener(
      new OnCompleteListener<Void>() {
       @Override
       public void onComplete(@NonNull Task<Void> task) {
         if (task.isSuccessful()) {
          // Successfully un-enrolled.
         }
       }
      });

在某些情況下,使用者在移除第二個驗證因素後可能會登出。請使用 onAuthStateChanged() 監聽此情況,並要求使用者重新登入。

後續步驟