将多个提供商与一个账号相关联

本文档介绍如何将多个提供商关联到单个 Identity Platform 账号。

Identity Platform 使用唯一 ID 来标识用户。这样,用户便可以通过不同的提供商登录同一账号。例如,最初使用电话号码注册的用户可以在之后关联其 Google 账号,然后使用任一方法登录。

准备工作

为您的应用增加对两个或多个身份提供商的支持。

启用或停用账号关联

账号关联设置决定了 Identity Platform 如何处理尝试使用相同电子邮件通过不同提供商登录的用户。

  • 关联使用相同电子邮件地址的账号:如果用户尝试使用已在使用的电子邮件地址登录,则 Identity Platform 会报错。您的应用可以捕获此错误,并将新的提供商关联到其现有账号。

  • 为每个身份提供商创建多个账号:每次用户通过不同的提供商登录时,系统都会创建一个新的 Identity Platform 用户账号。

要选择设置,请执行以下操作:

  1. 前往 Google Cloud 控制台中的 Identity Platform 设置页面。

    转到“设置”页面

  2. 选择用户账号关联下的设置。

  3. 点击保存

关联联合提供商凭据

如需关联某个联合提供商的凭据,请执行以下操作:

  1. 使用任意身份验证提供商或方法登录用户。

  2. 获取要将其与用户账号相关联的提供商所对应的提供商对象。例如:

    Web 版本 9

    import { GoogleAuthProvider, FacebookAuthProvider, TwitterAuthProvider, GithubAuthProvider } from "firebase/auth";
    
    const googleProvider = new GoogleAuthProvider();
    const facebookProvider = new FacebookAuthProvider();
    const twitterProvider = new TwitterAuthProvider();
    const githubProvider = new GithubAuthProvider();

    Web 版本 8

    var googleProvider = new firebase.auth.GoogleAuthProvider();
    var facebookProvider = new firebase.auth.FacebookAuthProvider();
    var twitterProvider = new firebase.auth.TwitterAuthProvider();
    var githubProvider = new firebase.auth.GithubAuthProvider();
  3. 提示用户使用要关联的提供方进行登录。您可以打开一个弹出式窗口,也可以重定向当前网页。对于移动设备用户而言,重定向更为便捷。

    如需显示弹出式窗口,请调用 linkWithPopup()

    Web 版本 9

    import { getAuth, linkWithPopup, GoogleAuthProvider } from "firebase/auth";
    const provider = new GoogleAuthProvider();
    
    const auth = getAuth();
    linkWithPopup(auth.currentUser, provider).then((result) => {
      // Accounts successfully linked.
      const credential = GoogleAuthProvider.credentialFromResult(result);
      const user = result.user;
      // ...
    }).catch((error) => {
      // Handle Errors here.
      // ...
    });

    Web 版本 8

    auth.currentUser.linkWithPopup(provider).then((result) => {
      // Accounts successfully linked.
      var credential = result.credential;
      var user = result.user;
      // ...
    }).catch((error) => {
      // Handle Errors here.
      // ...
    });

    如需重定向页面,请先调用 linkWithRedirect()

    使用 signInWithRedirectlinkWithRedirectreauthenticateWithRedirect 时,请遵循最佳实践

    Web 版本 9

    import { getAuth, linkWithRedirect, GoogleAuthProvider } from "firebase/auth";
    const provider = new GoogleAuthProvider();
    
    const auth = getAuth();
    linkWithRedirect(auth.currentUser, provider)
      .then(/* ... */)
      .catch(/* ... */);

    Web 版本 8

    auth.currentUser.linkWithRedirect(provider)
      .then(/* ... */)
      .catch(/* ... */);

    用户登录后会被其重定向回您的应用。然后,您可以通过调用 getRedirectResult() 来检索登录结果:

    Web 版本 9

    import { getRedirectResult } from "firebase/auth";
    getRedirectResult(auth).then((result) => {
      const credential = GoogleAuthProvider.credentialFromResult(result);
      if (credential) {
        // Accounts successfully linked.
        const user = result.user;
        // ...
      }
    }).catch((error) => {
      // Handle Errors here.
      // ...
    });

    Web 版本 8

    auth.getRedirectResult().then((result) => {
      if (result.credential) {
        // Accounts successfully linked.
        var credential = result.credential;
        var user = result.user;
        // ...
      }
    }).catch((error) => {
      // Handle Errors here.
      // ...
    });

用户使用联合提供商的用户账号现已关联至其 Identity Platform 账号,用户可以通过该提供商登录。

关联电子邮件和密码凭据

要为现有用户账号添加电子邮件地址和密码,请执行以下操作:

  1. 使用任意身份提供商或方法登录用户。

  2. 提示用户输入电子邮件地址和密码。

  3. 使用电子邮件地址和密码创建一个 AuthCredential 对象:

    Web 版本 9

    import { EmailAuthProvider } from "firebase/auth";
    
    const credential = EmailAuthProvider.credential(email, password);

    Web 版本 8

    var credential = firebase.auth.EmailAuthProvider.credential(email, password);
  4. AuthCredential 对象传递给已登录用户的 linkWithCredential() 方法:

    Web 版本 9

    import { getAuth, linkWithCredential } from "firebase/auth";
    
    const auth = getAuth();
    linkWithCredential(auth.currentUser, credential)
      .then((usercred) => {
        const user = usercred.user;
        console.log("Account linking success", user);
      }).catch((error) => {
        console.log("Account linking error", error);
      });

    Web 版本 8

    auth.currentUser.linkWithCredential(credential)
      .then((usercred) => {
        var user = usercred.user;
        console.log("Account linking success", user);
      }).catch((error) => {
        console.log("Account linking error", error);
      });

电子邮件和密码凭据现在已与用户的 Identity Platform 账号关联,用户可以使用这些凭据进行登录。

请注意,联合身份提供方凭据可以与使用其他电子邮件的电子邮件地址/密码账号相关联。如果发生这种情况,您可以使用与该联署提供商对应的电子邮件地址创建一个单独的电子邮件地址/密码账号。

处理“account-exists-with-different-credential”错误

如果您在 Google Cloud 控制台中启用了关联使用相同电子邮件地址的账号设置,当用户尝试使用一个提供商(例如 Google)中已存在的电子邮件登录另一个提供商(例如 SAML)时,系统会抛出错误 auth/account-exists-with-different-credential(以及 AuthCredential 对象)。

如需处理此错误,请提示用户使用现有提供方登录。然后调用 linkWithCredential()linkWithPopup()linkWithRedirect() 使用 AuthCredential 将新的提供商关联到他们的账号。

以下示例展示了如何在用户尝试通过 Facebook 登录时处理此错误:

Web 版本 9

  import { signInWithPopup, signInWithEmailAndPassword, linkWithCredential } from "firebase/auth";

  // User tries to sign in with Facebook.
  signInWithPopup(auth, facebookProvider).catch((error) => {
  // User's email already exists.
  if (error.code === 'auth/account-exists-with-different-credential') {
    // The pending Facebook credential.
    const pendingCred = error.credential;
    // The provider account's email address.
    const email = error.customData.email;

    // Present the user with a list of providers they might have
    // used to create the original account.
    // Then, ask the user to sign in with the existing provider.
    const method = promptUserForSignInMethod();

    if (method === 'password') {
      // TODO: Ask the user for their password.
      // In real scenario, you should handle this asynchronously.
      const password = promptUserForPassword();
      signInWithEmailAndPassword(auth, email, password).then((result) => {
        return linkWithCredential(result.user, pendingCred);
      }).then(() => {
        // Facebook account successfully linked to the existing user.
        goToApp();
      });
      return;
    }

    // All other cases are external providers.
    // Construct provider object for that provider.
    // TODO: Implement getProviderForProviderId.
    const provider = getProviderForProviderId(method);
    // At this point, you should let the user know that they already have an
    // account with a different provider, and validate they want to sign in
    // with the new provider.
    // Note: Browsers usually block popups triggered asynchronously, so in
    // real app, you should ask the user to click on a "Continue" button
    // that will trigger signInWithPopup().
    signInWithPopup(auth, provider).then((result) => {
      // Note: Identity Platform doesn't control the provider's sign-in
      // flow, so it's possible for the user to sign in with an account
      // with a different email from the first one.

      // Link the Facebook credential. We have access to the pending
      // credential, so we can directly call the link method.
      linkWithCredential(result.user, pendingCred).then((userCred) => {
        // Success.
        goToApp();
      });
    });
  }
});

Web 版本 8

  // User tries to sign in with Facebook.
      auth.signInWithPopup(facebookProvider).catch((error) => {
  // User's email already exists.
  if (error.code === 'auth/account-exists-with-different-credential') {
    // The pending Facebook credential.
    const pendingCred = error.credential;
    // The provider account's email address.
    const email = error.email;

    // Present the user with a list of providers they might have
    // used to create the original account.
    // Then, ask the user to sign in with the existing provider.
    const method = promptUserForSignInMethod();

    if (method === 'password') {
      // TODO: Ask the user for their password.
      // In real scenario, you should handle this asynchronously.
      const password = promptUserForPassword();
      auth.signInWithEmailAndPassword(email, password).then((result) => {
        return result.user.linkWithCredential(pendingCred);
      }).then(() => {
        // Facebook account successfully linked to the existing user.
        goToApp();
      });
      return;
    }

    // All other cases are external providers.
    // Construct provider object for that provider.
    // TODO: Implement getProviderForProviderId.
    const provider = getProviderForProviderId(method);
    // At this point, you should let the user know that they already have an
    // account with a different provider, and validate they want to sign in
    // with the new provider.
    // Note: Browsers usually block popups triggered asynchronously, so in
    // real app, you should ask the user to click on a "Continue" button
    // that will trigger signInWithPopup().
    auth.signInWithPopup(provider).then((result) => {
      // Note: Identity Platform doesn't control the provider's sign-in
      // flow, so it's possible for the user to sign in with an account
      // with a different email from the first one.

      // Link the Facebook credential. We have access to the pending
      // credential, so we can directly call the link method.
      result.user.linkWithCredential(pendingCred).then((userCred) => {
        // Success.
        goToApp();
      });
    });
  }
});

使用重定向与弹出式窗口类似,区别是您需要缓存在页面重定向之间待处理的凭据(例如,使用会话存储)。

请注意,一些提供商(如 Google 和 Microsoft)既是电子邮件服务提供商,也是社交身份提供商。电子邮件服务提供商相对其托管的电子邮件网域的所有相关地址而言具有权威性。这意味着使用同一提供商托管的电子邮件地址登录的用户绝不会引发此错误(例如,使用 @gmail.com 电子邮件通过 Google 登录,或使用 @live.com@outlook.com 电子邮件通过 Microsoft 登录)。

手动合并账号

如果用户尝试使用已关联到其他用户账号的凭据通过同一个提供商登录,则客户端 SDK 的内置账号关联方法将失败。在这种情况下,您需要手动合并账号,然后删除第二个账号。例如:

Web 版本 9

// Sign in first account.
const result1 = await signInWithCredential(auth, cred1);
const user1 = result1.user;
// Try to link a credential that belongs to an existing account
try {
  await linkWithCredential(user1, cred2);
} catch (error) {
  // cred2 already exists so an error is thrown.
  const result2 = await signInWithCredential(auth, error.credential);
  const user2 = result2.user;
  // Merge the data.
  mergeData(user1, user2);
  // Delete one of the accounts, and try again.
  await user2.delete();
  // Linking now will work.
  await linkWithCredential(user1, result2.credential);
}

Web 版本 8

// Sign in first account.
const result1 = await auth.signInWithCredential(cred1);
const user1 = result1.user;
// Try to link a credential that belongs to an existing account
try {
  await user1.linkWithCredential(cred2);
} catch (error) {
  // cred2 already exists so an error is thrown.
  const result2 = await auth.signInWithCredential(error.credential);
  const user2 = result2.user;
  // Merge the data.
  mergeData(user1, user2);
  // Delete one of the accounts, and try again.
  await user2.delete();
  // Linking now will work.
  await user1.linkWithCredential(result2.credential);
}

您可以解除提供商与用户账号的关联。用户将无法再通过该提供商进行身份验证。

如需解除与提供商的关联,请将提供商 ID 传递给 unlink() 方法。您可以从 providerData 属性中获取与用户相关联的身份验证提供方的 ID。

Web 版本 9

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

const auth = getAuth();
unlink(auth.currentUser, providerId).then(() => {
  // Auth provider unlinked from account
  // ...
}).catch((error) => {
  // An error happened
  // ...
});

Web 版本 8

user.unlink(providerId).then(() => {
  // Auth provider unlinked from account
  // ...
}).catch((error) => {
  // An error happened
  // ...
});

后续步骤