创建自定义登录页面

本文介绍了如何使用外部身份和 IAP 构建自己的身份验证页面。亲自构建此页面可让您完全控制身份验证流程和用户体验。

如果您不需要完全自定义界面,可以让 IAP 为您托管登录页面,或使用 FirebaseUI 获得更简洁的体验。

概览

如需构建自己的身份验证页面,请按以下步骤操作:

  1. 启用外部身份。在设置期间选择我将提供自己的界面选项
  2. 安装 gcip-iap
  3. 通过实现 AuthenticationHandler 接口配置界面。您的身份验证页面必须处理以下场景:
    • 租户选择
    • 用户授权
    • 用户登录
    • 错误处理
  4. 可选:使用其他功能(例如进度条、退出登录页面和用户处理)自定义身份验证页面
  5. 测试界面

安装 gcip-iap 库

如需安装 gcip-iap 库,请运行以下命令:

npm install gcip-iap --save

gcip-iap NPM 模块让您无需操心应用、IAP 和 Identity Platform 之间的通信。借助它,您可以自定义整个身份验证流程,而无需管理界面和 IAP 之间的基础交换。

使用适合您的 SDK 版本的导入:

gcip-iap v0.1.4 或更低版本

// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';

gcip-iap v1.0.0 升级到 v1.1.0

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

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

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

gcip-iap v2.0.0

从 2.0.0 版开始,gcip-iap 要求您使用模块化 SDK 格式重写自定义界面应用。如果您要迁移到 gcip-iap 2.0.0 版或更高版本,请执行以下操作:

  • package.json 文件中的 firebase 版本更新为 v9.8.3 及更高版本。
  • 按如下所示更新 firebase 导入语句:
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

配置界面

如需配置界面,请创建一个实现 AuthenticationHandler 接口的自定义类:

interface AuthenticationHandler {
  languageCode?: string | null;
  getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
  startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
  selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
  completeSignOut(): Promise<void>;
  processUser?(user: User): Promise<User>;
  showProgressBar?(): void;
  hideProgressBar?(): void;
  handleError?(error: Error | CIAPError): void;
}

在身份验证期间,该库会自动调用 AuthenticationHandler 的方法。

选择租户

如需选择租户,请实现 selectTenant()。您可以实现此方法以通过编程方式选择租户,也可以显示界面以便用户自行选择一个。

无论是哪种情况,该库都会使用返回的 SelectedTenantInfo 对象来完成身份验证流程。它包含所选租户的 ID、所有提供商 ID 以及用户输入的电子邮件地址。

如果您的项目中有多个租户,您必须先选择一个租户,然后才能对用户进行身份验证。如果您只有一个租户,或者正在使用项目级层身份验证,则无需实现 selectTenant()

IAP 支持的提供商与 Identity Platform 相同,例如:

  • 电子邮件地址和密码
  • OAuth(Google、Facebook、Twitter、GitHub、Microsoft 等)
  • SAML
  • OIDC
  • 电话号码
  • 自定义
  • 匿名

多租户不支持电话号码、自定义和匿名身份验证类型。

以编程方式选择租户

如需以编程方式选择租户,请利用当前上下文。Authentication 类包含 getOriginalURL(),该方法会返回用户在进行身份验证之前正访问的网址。

您可以使用此方法从关联的租户列表中找到匹配项:

// Select provider programmatically.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    auth.getOriginalURL()
      .then((originalUrl) => {
        resolve({
          tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
          // If associated provider IDs can also be determined,
          // populate this list.
          providerIds: [],
        });
      })
      .catch(reject);
  });
}

允许用户选择租户

如需允许用户选择租户,请显示租户列表,让用户选择一个,也可以要求他们输入自己的电子邮件地址,然后根据网域查找匹配项:

// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    renderSelectTenant(
        tenantIds,
        // On tenant selection.
        (selectedTenantId) => {
          resolve({
            tenantId: selectedTenantId,
            // If associated provider IDs can also be determined,
            // populate this list.
            providerIds: [],
            // If email is available, populate this field too.
            email: undefined,
          });
        });
  });
}

对用户进行身份验证

创建提供程序后,实现 getAuth() 以返回与提供的 API 密钥和租户 ID 对应的 Auth 实例。如果未提供任何租户 ID,请使用项目级身份提供程序。

getAuth() 会跟踪与提供的配置对应的用户的存储位置。它还使得可以静默刷新先前已认证的用户的 Identity Platform ID 令牌,而无需用户重新输入其凭据。

如果您将多个 IAP 资源用于不同的租户,我们建议您为每个资源使用唯一的身份验证实例。这允许具有不同配置的多个资源使用同一身份验证页面。它还允许多个用户同时登录而无需退出前一个用户。

以下是一个有关如何实现 getAuth() 的示例:

gcip-iap v1.0.0

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = firebase.app(tenantId || undefined).auth();
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = app.auth();
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

gcip-iap v2.0.0

import {initializeApp, getApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = getAuth(getApp(tenantId || undefined));
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = getAuth(app);
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

让用户登录

如需处理登录,请实现 startSignIn(),显示一个供用户进行身份验证的界面,然后在完成时为已登录的用户返回 UserCredential

在多租户环境中,您可以根据 SelectedTenantInfo(如果已提供)确定可用的身份验证方法。该变量包含由 selectTenant() 返回的相同信息。

以下示例展示了使用电子邮件地址和密码为现有用户实现 startSignIn()

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
      auth.signInWithEmailAndPassword(email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

gcip-iap v2.0.0

import {signInWithEmailAndPassword} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
        signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

您还可以用弹出式窗口或重定向通过联盟提供商(例如 SAML 或 OIDC)让用户登录:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in may have already been determined from the
    // selectedTenantInfo object.
    const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    auth.signInWithRedirect(provider);
  });
}

gcip-iap v2.0.0

import {signInWithPopup, SAMLAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in might have already been determined from the
    // selectedTenantInfo object.
    const provider = new SAMLAuthProvider('saml.myProvider');
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    signInWithRedirect(auth, provider);
  });
}

一些 OAuth 提供商支持传递登录提示以进行登录:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new firebase.auth.OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

gcip-iap v2.0.0

import {signInWithPopup, OAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign in or sign up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

如需了解详情,请参阅使用多租户进行身份验证

处理错误

如需向用户显示错误消息或尝试从网络超时等错误中恢复,请实现 handleError()

以下示例实现了 handleError()

handleError(error) {
  showAlert({
    code: error.code,
    message: error.message,
    // Whether to show the retry button. This is only available if the error is
    // recoverable via retrial.
    retry: !!error.retry,
  });
  // When user clicks retry, call error.retry();
  $('.alert-link').on('click', (e) => {
    error.retry();
    e.preventDefault();
    return false;
  });
}

下表列出了可能会返回的特定于 IAP 的错误代码。Identity Platform 也可能返回错误;请参阅 firebase.auth.Auth 的文档。

错误代码 说明
invalid-argument 客户端指定了无效参数。
failed-precondition 请求无法在当前系统状态下执行。
out-of-range 客户端指定了无效范围。
unauthenticated 由于缺少、无效或过期的 OAuth 令牌,请求未通过身份验证。
permission-denied 客户端没有足够的权限,或者界面托管在未经授权的网域中。
not-found 未找到指定的资源。
aborted 并发冲突,例如读取/修改/写入冲突。
already-exists 客户端尝试创建的资源已存在。
resource-exhausted 资源配额不足或达到了速率限制。
cancelled 请求被客户端取消。
data-loss 出现了不可恢复的数据丢失或数据损坏问题。
unknown 出现未知的服务器错误。
internal 内部服务器错误。
not-implemented API 方法未通过服务器实现。
unavailable 服务不可用。
restart-process 重新访问将您重定向到此页面的网址,以重新启动身份验证流程。
deadline-exceeded 已超过请求时限。
authentication-uri-fail 未生成身份验证 URI。
gcip-token-invalid 提供了无效的 GCIP ID 令牌。
gcip-redirect-invalid 无效的重定向网址。
get-project-mapping-fail 未获取项目 ID。
gcip-id-token-encryption-error GCIP ID 令牌加密错误。
gcip-id-token-decryption-error GCIP ID 令牌解密错误。
gcip-id-token-unescape-error Web 安全 base64 unescape 失败。
resource-missing-gcip-sign-in-url 指定的 IAP 资源缺少 GCIP 身份验证网址。

自定义界面

您可以使用进度条和退出页面等可选功能来自定义身份验证页面。

显示进度界面

如需在 gcip-iap 模块执行长时间运行的网络任务时向用户显示自定义进度界面,请实现 showProgressBar()hideProgressBar()

让用户退出账号

在某些情况下,您可能希望允许用户从共享相同身份验证网址的所有当前会话中退出。

用户退出后,可能没有将其重定向回的网址。当用户从与登录页面关联的所有租户中退出时,通常会发生这种情况。在这种情况下,请实现 completeSignOut() 以显示一条消息,指示用户已成功退出。如果您未实现此方法,则在用户退出账号时会显示空白页。

处理用户

如需在重定向到 IAP 资源之前修改已登录的用户,请实现 processUser()

您可以使用此方法执行以下操作:

  • 链接到其他提供商。
  • 更新用户的个人资料。
  • 注册后要求用户提供其他数据。
  • 调用 signInWithRedirect() 之后,处理 getRedirectResult() 返回的 OAuth 访问令牌。

以下是实现 processUser() 的示例:

gcip-iap v1.0.0

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

gcip-iap v2.0.0

import {getRedirectResult} from 'firebase/auth';

processUser(user) {
  return getRedirectResult(lastAuthUsed).then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

如果您希望对用户进行的任何更改反映在由 IAP 传播到您的应用的 ID 令牌声明中,则必须强制刷新令牌:

gcip-iap v1.0.0

processUser(user) {
  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;
  });
}

gcip-iap v2.0.0

import {updateProfile} from 'firebase/auth';

processUser(user) {
  return updateProfile(user, {
    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;
  });
}

测试界面

创建用于实现 AuthenticationHandler 的类后,您就可以使用它来创建新的 Authentication 实例并启动它:

// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();

部署您的应用并转到身份验证页面。您应该会看到自定义登录界面。

后续步骤