在多租户环境中对用户进行身份验证

本文档介绍如何在多租户 Identity Platform 环境中对用户进行身份验证。

准备工作

请确保您已为项目启用了多租户并配置了租户。如需了解具体方法,请参阅多租户使用入门

您还需要将 Client SDK 添加到您的应用中:

  1. 转到 Cloud Console 中的 Identity Platform 页面。
    转到 Identity Platform 用户页面

  2. 点击右上角的应用设置详情

  3. 将代码复制到您的 Web 应用中。例如:

    <script src="https://www.gstatic.com/firebasejs/x.x.x/firebase.js"></script>
    <script>
    // Initialize Identity Platform
    const config = {
      apiKey: "...",
      authDomain: "my-app-12345.firebaseapp.com"
    };
    firebase.initializeApp(config);
    </script>
    

使用租户登录

要登录到租户,需要将租户 ID 传递给 auth 对象。请注意,重新加载页面时不会保留 tenantId

var tenantId = TENANT_ID1;
firebase.auth().tenantId = tenantId;

auth 实例以后发出的任何登录请求都将包含该租户 ID(在以上示例中为 TENANT_ID1),除非您更改或重置租户 ID。

您可以使用单个或多个 auth 实例处理多个租户。

如需使用单个 auth 实例,则每当您要在租户之间切换时,修改 tenantId 属性即可。要还原为项目级 IdP,请将 tenantId 设置为 null

// One Auth instance
// Switch to tenant1
firebase.auth().tenantId = 'tenant1';
// Switch to tenant2
firebase.auth().tenantId = 'tenant2';
// Switch back to project level IdPs
firebase.auth().tenantId = null;

如需使用多个实例,请为每个租户创建一个新的 auth 实例并为其分配不同的 ID:

// Multiple Auth instances
firebase.initializeApp(config, 'app1_for_tenantId1');
firebase.initializeApp(config, 'app2_for_tenantId2');

const auth1 = firebase.app('app1').auth();
const auth2 = firebase.app('app2').auth();

auth1.tenantId = 'tenant1';
auth2.tenantId = 'tenant2';

通过租户登录后,系统将返回租户用户并将 user.tenantId 设置为该租户。请注意,如果您以后在 auth 实例上切换 tenantIdcurrentUser 属性不会发生变化;它仍将指向与之前的租户相同的用户。

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

// Sign in with tenant
firebase.auth().signInWithEmailAndPassword(email, password)
  .then((result) => {
    const user = result.user;
    // user.tenantId is set to 'TENANT_ID1'.
    // Switch to 'TENANT_ID2'.
    firebase.auth().tenantId = 'TENANT_ID2';
    // firebase.auth().currentUser still point to the user.
    // firebase.auth().currentUser.tenantId is 'TENANT_ID1'.
  });

// You could also get the current user from Auth state observer.
firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    // User is signed in.
    // user.tenantId is set to 'TENANT_ID1'.
  } else {
    // No user is signed in.
  }
});

电子邮件/密码帐号

以下示例展示了如何注册新用户:

firebase.auth().tenantId = 'TENANT_ID';

firebase.auth().createUserWithEmailAndPassword(email, password)
  .then((result) => {
    // result.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle error.
  });

让现有用户登录:

firebase.auth().tenantId = 'TENANT_ID';

firebase.auth().signInWithEmailAndPassword(email, password)
  .then((result) => {
    // result.user.tenantId is 'TENANT_ID'.
  }).catch((error) => {
    // Handle error.
  });

SAML

如需通过 SAML 提供商登录,请使用 Cloud Console 中的提供商 ID 实例化 SAMLAuthProvider 实例:

const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');

然后,您可以使用弹出式窗口或重定向流程登录 SAML 提供商。

以下示例展示了弹出式窗口流程:

// Switch to TENANT_ID1.
firebase.auth().tenantId = 'TENANT_ID1';

//Sign-in with popup.
firebase.auth().signInWithPopup(provider)
  .then((result) => {
    // User is signed in.
    // tenant ID is available in result.user.tenantId.
    // Identity provider data is available in result.additionalUserInfo.profile.
  })
  .catch((error) => {
    // Handle error.
  });

重定向

以下示例展示了重定向流程:

// Switch to TENANT_ID1.
firebase.auth().tenantId = 'TENANT_ID1';

// Sign-in with redirect.
firebase.auth().signInWithRedirect(provider);

// After the user completes sign-in and returns to the app, you can get
// the sign-in result by calling getRedirectResult. However, if they sign out
// and sign in again with an IdP, no tenant is used.
firebase.auth().getRedirectResult()
  .then((result) => {
    // User is signed in.
    // The tenant ID available in result.user.tenantId.
    // Identity provider data is available in result.additionalUserInfo.profile.
  })
  .catch((error) => {
    // Handle error.
  });

在这两种情况下,请务必在 auth 实例上设置正确的租户 ID。

要启动身份验证流程,请向用户显示一个要求其提供电子邮件地址的界面,然后调用 sendSignInLinkToEmail 向用户发送身份验证链接。在发送电子邮件之前,请务必在 auth 实例上设置正确的租户 ID。

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
  .then(function() {
    // The link was successfully sent. Inform the user.
    // Save the email locally so you don't need to ask the user for it again
    // if they open the link on the same device.
    window.localStorage.setItem('emailForSignIn', email);
  })
  .catch(function(error) {
    // Some error occurred, you can inspect the code: error.code
  });

要在着陆页上完成登录,请首先解析电子邮件链接中的租户 ID,然后在 auth 实例上设置租户 ID。然后,使用用户的电子邮件和包含一次性验证码的实际电子邮件链接调用 signInWithEmailLink

if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
  const actionCodeUrl = firebase.auth.ActionCodeURL.parseLink(window.location.href);
  if (actionCodeUrl.tenantId) {
      firebase.auth().tenantId = actionCodeUrl.tenantId;
  }
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  firebase.auth().signInWithEmailLink(email, window.location.href)
    .then((result) => {
      // User is signed in.
      // tenant ID available in result.user.tenantId.
    });
}

创建自定义令牌

创建多租户感知自定义令牌与创建常规自定义令牌相同;只要在 auth 实例上设置了正确的租户 ID,顶级 tenant_id 声明就会被添加到生成的 JWT。如需详细了解如何创建和使用自定义令牌,请参阅创建自定义令牌

以下示例展示了如何使用 Admin SDK 创建自定义令牌:

// Ensure you're using a tenant-aware auth instance
const tenantManager = admin.auth().tenantManager();
const tenantAuth = tenantManager.authForTenant('TENANT_ID1');

// Create a custom token in the usual manner
tenantAuth.createCustomToken(uid)
  .then((customToken) => {
    // Send token back to client
  })
  .catch((error) => {
    console.log('Error creating custom token:', error);
  });

以下代码演示了如何使用自定义令牌登录:

firebase.auth().tenantId = 'TENANT_ID1';

firebase.auth().signInWithCustomToken(token)
  .catch((error) => {
    // Handle Errors here.
    var errorCode = error.code;
    var errorMessage = error.message;
    // ...
  });

请注意,如果租户 ID 不匹配,signInWithCustomToken() 方法将失败。

关联多租户用户凭据

您可以将其他类型的凭据与现有的多租户用户关联。例如,如果用户之前已通过一个租户中的 SAML 提供商进行身份验证,您可以向用户的现有帐号添加电子邮件/密码登录,以便他们可以使用任一方法登录租户。

// Switch to TENANT_ID1
firebase.auth().tenantId = 'TENANT_ID1';

//Sign-in with popup
firebase.auth().signInWithPopup(provider)
  .then((result) => {
    // Existing user with SAML provider.
    const user = result.user;
    const emailCredential =
        firebase.auth.EmailAuthProvider.credential(email, password);
    return user.linkWithCredential(emailCredential);
  })
  .then((linkResult) => {
    // The user can sign in with both SAML and email/password now.
  });

关联现有的多租户用户或重新验证其身份时,auth.tenantId 会被忽略;请使用 user.tenantId 指定要使用的租户。这也适用于其他用户管理 API,如 updateProfileupdatePassword

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

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

要通过所需的提供商完成登录,用户必须先登录现有提供商 (Google),然后关联至之前的 AuthCredential (SAML)。

您可以使用弹出式窗口或重定向流程来处理此错误。

以下示例展示了如何在使用 signInWithPopup 时处理 auth/account-exists-with-different-credential 错误:

// Step 1.
// User tries to sign in to the SAML provider in that tenant.
firebase.auth().tenantId = 'TENANT_ID';
firebase.auth().signInWithPopup(samlProvider)
  .catch((error) => {
    // An error happened.
    if (error.code === 'auth/account-exists-with-different-credential') {
      // Step 2.
      // User's email already exists.
      // The pending SAML credential.
      var pendingCred = error.credential;
      // The credential's tenantId if needed: error.tenantId
      // The provider account's email address.
      var email = error.email;
      // Get sign-in methods for this email.
      firebase.auth().fetchSignInMethodsForEmail(email)
        .then((methods) => {
          // Step 3.
          // Ask the user to sign in with existing Google account.
          if (methods[0] == 'google.com') {
            firebase.auth().signInWithPopup(googleProvider)
              .then((result) => {
                // Step 4
                // Link the SAML AuthCredential to the existing user.
                result.user.linkWithCredential(pendingCred)
                  .then(function(linkResult) {
                    // SAML account successfully linked to the existing
                    // user.
                    goToApp();
                  });
              });
          }
        });
    }
  });

重定向

使用 signInWithRedirect 时,在完成重定向流程时,将在 getRedirectResult 中引发 auth/account-exists-with-different-credential 错误。

错误对象包含属性 error.tenantId。由于 auth 实例上的租户 ID 在重定向后不会保留,因此您需要在 auth 实例上设置错误对象中的租户 ID。

以下示例展示了如何处理错误:

// Step 1.
// User tries to sign in to SAML provider.
firebase.auth().tenantId = 'TENANT_ID';
firebase.auth().signInWithRedirect(samlProvider);
// Redirect back from SAML IDP. auth.tenantId is null after redirecting.
firebase.auth().getRedirectResult().catch((error) => {
  if (error.code === 'auth/account-exists-with-different-credential') {
    // Step 2.
    // User's email already exists.
    var tenantId = error.tenantId;
    // The pending SAML credential.
    var pendingCred = error.credential;
    // The provider account's email address.
    var email = error.email;
    // Need to set the tenant ID again as the page was reloaded and the
    // previous setting was reset.
    firebase.auth().tenantId = tenantId;
    // Get sign-in methods for this email.
    firebase.auth().fetchSignInMethodsForEmail(email)
      .then((methods) => {
        // Step 3.
        // Ask the user to sign in with existing Google account.
        if (methods[0] == 'google.com') {
          firebase.auth().signInWithRedirect(googleProvider);
        }
      });
  }
});

// Redirect back from Google. auth.tenantId is null after redirecting.
firebase.auth().getRedirectResult().then((result) => {
  // Step 4
  // Link the SAML AuthCredential to the existing user.
  // result.user.tenantId is 'TENANT_ID'.
  result.user.linkWithCredential(pendingCred)
    .then(function(linkResult) {
      // SAML account successfully linked to the existing
      // user.
      goToApp();
    });
});

后续步骤