使用 FirebaseUI 创建登录页面

要将外部身份与 Identity-Aware Proxy (IAP) 搭配使用,您的应用需要登录页面。IAP 会将用户重定向到此页面进行身份验证,然后用户才能访问安全资源。

本文介绍如何使用 FirebaseUI(一个开源 JavaScript 库)来构建身份验证页面。FirebaseUI 提供了可自定义的元素来帮助减少样板代码,并处理用户登录流程,这些用户使用各种身份提供者登录。

为了更快开始使用,请让 IAP 为您托管界面。这可让您尝试使用外部身份,而无需编写任何其他代码。对于更高级的场景,您还可以从头开始构建您自己的登录页面。此选项较为复杂,但可让您完全控制身份验证流程和用户体验。

准备工作

启用外部身份,并在设置期间选择我将提供自己的界面选项。

安装库

安装 gcip-iapfirebasefirebaseui 库。gcip-iap 模块抽象了应用、IAP 和 Identity Platform 之间的通信。firebasefirebaseui 库提供了身份验证界面的基础组件。

npm install firebase --save
npm install firebaseui --save
npm install gcip-iap --save

请注意,gcip-iap 模块不适用于 CDN。

然后,您可以在源文件中 import 这些模块。使用适合您的 SDK 版本的导入:

gcip-iap v0.1.4 或更低版本

// Import firebase modules.
import * as firebase from "firebase/app";
import "firebase/auth";
// Import firebaseui module.
import * as firebaseui from 'firebaseui'
// Import gcip-iap module.
import * as ciap from 'gcip-iap';

gcip-iap v1.0.0 或更高版本

从 1.0.0 版开始,gcip-iap 需要 firebase 第 9 版对等依赖项或更高版本。如果您要迁移到 gcip-iap 1.0.0 版或更高版本,请执行以下操作:

  • package.json 文件中的 firebasefirebaseui 版本分别更新为 9.6.0 版及更高版本和 6.0.0 版及更高版本。
  • 按如下所示更新 firebase 导入语句:
// Import firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import firebaseui module.
import * as firebaseui from 'firebaseui'
// Import gcip-iap module.

无需进行额外的代码更改。

有关其他安装选项(包括如何使用库的本地化版本),请参阅 GitHub 上的说明

配置应用

FirebaseUI 使用一个配置对象,该对象指定用于身份验证的租户和提供商。完整的配置可能很长,可能看起来如下所示:

// The project configuration.
const configs = {
  // Configuration for project identified by API key API_KEY1.
  API_KEY1: {
    authDomain: 'project-id1.firebaseapp.com',
    // Decide whether to ask user for identifier to figure out
    // what tenant to select or whether to present all the tenants to select from.
    displayMode: 'optionFirst', // Or identifierFirst
    // The terms of service URL and privacy policy URL for the page
    // where the user select tenant or enter email for tenant/provider
    // matching.
    tosUrl: 'http://localhost/tos',
    privacyPolicyUrl: 'http://localhost/privacypolicy',
    callbacks: {
      // The callback to trigger when the selection tenant page
      // or enter email for tenant matching page is shown.
      selectTenantUiShown: () => {
        // Show title and additional display info.
      },
      // The callback to trigger when the sign-in page
      // is shown.
      signInUiShown: (tenantId) => {
        // Show tenant title and additional display info.
      },
      beforeSignInSuccess: (user) => {
        // Do additional processing on user before sign-in is
        // complete.
        return Promise.resolve(user);
      }
    },
    tenants: {
      // Tenant configuration for tenant ID tenantId1.
      tenantId1: {
        // Full label, display name, button color and icon URL of the
        // tenant selection button. Only needed if you are
        // using the option first option.
        fullLabel: 'ACME Portal',
        displayName: 'ACME',
        buttonColor: '#2F2F2F',
        iconUrl: '<icon-url-of-sign-in-button>',
         // Sign-in providers enabled for tenantId1.
        signInOptions: [
          // Microsoft sign-in.
          {
            provider: 'microsoft.com',
            providerName: 'Microsoft',
            buttonColor: '#2F2F2F',
            iconUrl: '<icon-url-of-sign-in-button>',
            loginHintKey: 'login_hint'
          },
          // Email/password sign-in.
          {
            provider: 'password',
            // Do not require display name on sign up.
            requireDisplayName: false,
            disableSignUp: {
              // Disable user from signing up with email providers.
              status: true,
              adminEmail: 'admin@example.com',
              helpLink: 'https://www.example.com/trouble_signing_in'
            }
          },
          // SAML provider. (multiple SAML providers can be passed)
          {
            provider: 'saml.my-provider1',
            providerName: 'SAML provider',
            fullLabel: 'Employee Login',
            buttonColor: '#4666FF',
            iconUrl: 'https://www.example.com/photos/my_idp/saml.png'
          },
        ],
        // If there is only one sign-in provider eligible for the user,
        // whether to show the provider selection page.
        immediateFederatedRedirect: true,
        signInFlow: 'redirect', // Or popup
        // The terms of service URL and privacy policy URL for the sign-in page
        // specific to each tenant.
        tosUrl: 'http://localhost/tenant1/tos',
        privacyPolicyUrl: 'http://localhost/tenant1/privacypolicy'
      },
      // Tenant configuration for tenant ID tenantId2.
      tenantId2: {
        fullLabel: 'OCP Portal',
        displayName: 'OCP',
        buttonColor: '#2F2F2F',
        iconUrl: '<icon-url-of-sign-in-button>',
        // Tenant2 supports a SAML, OIDC and Email/password sign-in.
        signInOptions: [
          // Email/password sign-in.
          {
            provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
            // Do not require display name on sign up.
            requireDisplayName: false
          },
          // SAML provider. (multiple SAML providers can be passed)
          {
            provider: 'saml.my-provider2',
            providerName: 'SAML provider',
            fullLabel: 'Contractor Portal',
            buttonColor: '#4666FF',
            iconUrl: 'https://www.example.com/photos/my_idp/saml.png'
          },
          // OIDC provider. (multiple OIDC providers can be passed)
          {
            provider: 'oidc.my-provider1',
            providerName: 'OIDC provider',
            buttonColor: '#4666FF',
            iconUrl: 'https://www.example.com/photos/my_idp/oidc.png'
          },
        ],
      },
      // Tenant configuration for tenant ID tenantId3.
      tenantId3: {
        fullLabel: 'Tenant3 Portal',
        displayName: 'Tenant3',
        buttonColor: '#007bff',
        iconUrl: '<icon-url-of-sign-in-button>',
        // Tenant3 supports a Google and Email/password sign-in.
        signInOptions: [
          // Email/password sign-in.
          {
            provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
            // Do not require display name on sign up.
            requireDisplayName: false
          },
          // Google provider.
          {
            provider: 'google.com',
            scopes: ['scope1', 'scope2', 'https://example.com/scope3'],
            loginHintKey: 'login_hint',
            customParameters: {
              prompt: 'consent',
            },
          },
        ],
        // Sets the adminRestrictedOperation configuration for providers
        // including federated, email/password, email link and phone number.
        adminRestrictedOperation: {
          status: true,
          adminEmail: 'admin@example.com',
          helpLink: 'https://www.example.com/trouble_signing_in'
        }
      },
    },
  },
};

以下各部分提供有关如何配置 IAP 专用字段的指导。有关设置其他字段的示例,请参见上面的代码段或 GitHub 上的 FirebaseUI 文档

设置 API 密钥

典型的配置以项目的 API 密钥开头:

// The project configuration.
const configs = {
  // Configuration for API_KEY.
  API_KEY: {
    // Config goes here
  }
}

在大多数情况下,您只需要指定一个 API 密钥即可。但是,如果您要在多个项目中使用单个身份验证网址,则可以添加多个 API 密钥:

const configs = {
  API_KEY1: {
    // Config goes here
  },
  API_KEY2: {
    // Config goes here
  },
}

获取身份验证网域

authdomain 字段设置为为方便联合登录而配置的网域。您可以从 Google Cloud 控制台中的 Identity Platform 页面检索此字段。

指定租户 ID

配置需要用户可用其进行身份验证的租户和提供商的列表。

每个租户都通过其 ID 进行标识。如果您使用的是项目级层身份验证(无租户),请改用特殊的 _ 标识符作为 API 密钥。例如:

const configs = {
  // Configuration for project identified by API key API_KEY1.
  API_KEY1: {
    tenants: {
      // Project-level IdPs flow.
      _: {
        // Tenant config goes here
      },
      // Single tenant flow.
      1036546636501: {
        // Tenant config goes here
      }
    }
  }
}

您还可以使用 * 运算符指定通配符租户配置。如果找不到匹配的 ID,则将该租户用作后备。

配置租户提供商

每个租户都有自己的提供商;后者在 signInOptions 字段中指定:

tenantId1: {
  signInOptions: [
    // Options go here
  ]
}

如需了解如何配置提供商,请参阅 FirebaseUI 文档中的配置登录提供商

除了 FirebaseUI 文档中概述的步骤外,还有一些 IAP 专用的字段,具体取决于您选择的租户选择模式。如需详细了解这些字段,请参见下一部分。

选择租户选择模式

用户可以通过两种方式选择租户:选项优先模式标识符优先模式

在选项模式下,用户首先从列表中选择一个租户,然后输入其用户名和密码。在标识符模式下,用户首先输入他们的电子邮件地址。然后,系统会自动选择具有与电子邮件地址网域匹配的身份提供商的第一位租户。

如需使用选项模式,请将 displayMode 设置为 optionFirst。然后,您需要为每个租户的按钮提供配置信息,包括 displayNamebuttonColoriconUrl。 您还可以选择提供一个 fullLabel,用于替换整个按钮标签,而不仅仅是显示名称。

以下是配置为使用选项模式的租户的示例:

tenantId1: {
  fullLabel: 'ACME Portal',
  displayName: 'ACME',
  buttonColor: '#2F2F2F',
  iconUrl: '<icon-url-of-sign-in-button>',
  // ...

要使用标识符模式,每个登录选项必须指定一个 hd 字段,以指示其支持的网域。这可以是正则表达式(例如 /@example\.com$/)或网域字符串(例如,example.com)。

以下代码显示了配置为使用标识符模式的租户:

tenantId1: {
  signInOptions: [
    // Email/password sign-in.
    {
      hd: 'acme.com', // using regex: /@acme\.com$/
      // ...
    },

启用即时重定向

如果您的应用仅支持单个身份提供商,则将 immediateFederatedRedirect 设置为 true 会跳过登录界面,并将用户直接重定向到该提供商。

设置回调

配置对象包含一组回调,这些回调在身份验证流程中的各个时间点被调用。这使您可以另外自定义界面。以下挂钩可用:

selectTenantUiShown() 在显示用于选择租户的界面时触发。如果要使用自定义标题或主题修改界面,请使用此选项。
signInUiShown(tenantId) 当选择租户并显示供用户输入其凭据的界面时触发。如果要使用自定义标题或主题修改界面,请使用此选项。
beforeSignInSuccess(user) 在登录完成之前触发。使用此方法可以在重定向回 IAP 资源之前修改已登录的用户。

以下示例代码演示了如何实现这些回调:

callbacks: {
  selectTenantUiShown: () => {
    // Show info of the IAP resource.
    showUiTitle(
        'Select your employer to access your Health Benefits');
  },
  signInUiShown: (tenantId) => {
    // Show tenant title and additional display info.
    const tenantName = getTenantNameFromId(tenantId);
    showUiTitle(`Sign in to access your ${tenantName} Health Benefits`);
  },
  beforeSignInSuccess: (user) => {
    // Do additional processing on user before sign-in is
    // complete.
    // For example update the user profile.
    return user.updateProfile({
      photoURL: 'https://example.com/profile/1234/photo.png',
    }).then(function() {
      // To reflect updated photoURL in the ID token, force token
      // refresh.
      return user.getIdToken(true);
    }).then(function() {
      return user;
    });
  }
}

初始化库

创建配置对象后,请按照以下步骤在身份验证页面上初始化库:

  1. 创建一个用于呈现界面的 HTML 容器。

    <!DOCTYPE html>
    <html>
     <head>...</head>
     <body>
       <!-- The surrounding HTML is left untouched by FirebaseUI.
            Your app may use that space for branding, controls and other
            customizations.-->
       <h1>Welcome to My Awesome App</h1>
       <div id="firebaseui-auth-container"></div>
     </body>
    </html>
    
  2. 创建一个 FirebaseUiHandler 实例以呈现在 HTML 容器中,并将创建的 config 元素传递给它。

    const configs = {
      // ...
    }
    const handler = new firebaseui.auth.FirebaseUiHandler(
      '#firebaseui-auth-container', configs);
    
  3. 创建一个新的 Authentication 实例,将处理程序传递给它,然后调用 start()

    const ciapInstance = new ciap.Authentication(handler);
    ciapInstance.start();
    

部署您的应用并转到身份验证页面。系统应该会显示一个包含您的租户和提供商的登录界面。

后续步骤