建立自訂登入頁面

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

如果不需要全面自訂 UI,可以讓 IAP 代管登入頁面,或使用 FirebaseUI 獲得更簡化的體驗。

總覽

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

  1. 啟用外部身分。在設定期間選取「I'll provide my own UI option」(我會自行提供 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 v1.0.0 至 v1.1.0

自 v1.0.0 版起,gcip-iap 需要 firebase v9 以上的對等依附元件。如要遷移至 gcip-iap v1.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

從 v2.0.0 版開始,gcip-iap 需要使用模組化 SDK 格式,重新編寫自訂 UI 應用程式。如要遷移至 gcip-iap v2.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、所有 provider 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(),顯示使用者驗證的 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 Google Cloud Identity Platform ID 權杖加密錯誤。
gcip-id-token-decryption-error Google Cloud Identity Platform ID 權杖解密錯誤。
gcip-id-token-unescape-error 無法解除網路安全 Base64 編碼。
resource-missing-gcip-sign-in-url 指定 IAP 資源缺少 GCIP 驗證網址。

自訂 UI

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

顯示進度 UI

每當 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;
  });
}

測試 UI

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

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

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

後續步驟