建立自訂登入頁面

本文將說明如何使用外部身分和 IAP 建構自己的驗證頁面。自行建構此頁面,即可完全掌控驗證流程和使用者體驗。

如果您不需要完全自訂 UI,可以讓 IAP 代管登入頁面,或是使用 FirebaseUI 打造更流暢的體驗。

總覽

如要自行建構驗證頁面,請按照下列步驟操作:

  1. 啟用外部身分。在設定期間選取「我會自行提供 UI 選項」
  2. 安裝 gcip-iap 程式庫
  3. 透過導入 AuthenticationHandler 介面設定 UI。驗證頁面必須處理以下情況:
    • 租戶選項
    • 使用者授權
    • 使用者登入
    • 處理錯誤
  4. 選用:使用其他功能自訂驗證頁面,例如進度列、登出頁面和使用者處理程序。
  5. 測試 UI

安裝 gcip-iap 程式庫

如要安裝 gcip-iap 程式庫,請執行下列指令:

npm install gcip-iap --save

gcip-iap NPM 模組會擷取應用程式、IAP 和 Identity Platform 之間的通訊。這樣一來,您就能自訂整個驗證流程,而無須管理 UI 和 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 1.0.0 至 1.1.0

自 1.0.0 版起,gcip-iap 需要 firebase v9 以上版本的對等依附元件。如果您要遷移至 gcip-iap v1.0.0 以上版本,請完成下列操作:

  • package.json 檔案中的 firebase 版本更新為 v9.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 格式重寫自訂 UI 應用程式。如果您要遷移至 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';

設定 UI

如要設定 UI,請建立實作 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()。您可以實作此方法,以程式設計方式選擇租用戶,或顯示 UI,讓使用者自行選取。

無論是哪種情況,程式庫都會使用傳回的 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,請使用專案層級的 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(),顯示 UI 供使用者驗證,然後在完成時,為已登入的使用者傳回 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 用戶端權限不足,或是 UI 託管在未經授權的網域上。
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 網路安全 Base64 解碼失敗。
resource-missing-gcip-sign-in-url 指定 IAP 資源缺少 GCIP 驗證網址。

自訂使用者介面

您可以使用進度列和登出頁面等選用功能自訂驗證頁面。

顯示進度 UI

如要讓 gcip-iap 模組執行長時間執行的網路工作時,向使用者顯示自訂進度 UI,請實作 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;
  });
}

測試 UI

建立實作 AuthenticationHandler 的類別後,您可以使用該類別建立新的 Authentication 例項,然後啟動它:

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

部署應用程式並前往驗證頁面。您應該會看到自訂登入 UI。

後續步驟