カスタム ログインページの作成

この記事では、外部 ID と IAP を使用して独自の認証ページを作成する方法について説明します。この認証ページを自分で作成すると、認証フローとユーザー エクスペリエンスを完全に制御できます。

UI を完全にカスタマイズする必要がない場合は、IAP でログインページをホストするか、もっと効率的なエクスペリエンスになるよう FirebaseUI を使用します。

概要

独自の認証ページを作成する手順は次のとおりです。

  1. 外部IDの有効化. セットアップ時に [独自に 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 のバージョンを v9.6.0 以降に更新してください。
  • firebase の import ステートメントを次のように更新します。
// 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 ステートメントを次のように更新します。
  // 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、ユーザーが入力したメールが含まれます。

プロジェクトに複数のテナントがある場合は、ユーザーを認証する前に 1 つを選択する必要があります。テナントが 1 つだけの場合や、プロジェクト レベルの認証を使用している場合は、selectTenant() を実装する必要はありません。

IAP は、Identity Platform と同じプロバイダをサポートしています。たとえば、次のものをサポートしています。

  • メールとパスワード
  • OAuth(Google、Facebook、Twitter、GitHub、Microsoft など)
  • SAML
  • OIDC
  • 電話番号
  • カスタム
  • 匿名

電話番号、カスタム、匿名の認証タイプはマルチテナントではサポートされていません。

プログラムによるテナントの選択

プログラムでテナントを選択する場合、現在のコンテキストを使用します。Authentication クラスには、認証前にユーザーがアクセスしていた URL を返す 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 認証プロセスを再開するには、このページにリダイレクトした URL をもう一度確認してください。
deadline-exceeded リクエスト期限を超えました。
authentication-uri-fail 認証 URI を生成できませんでした。
gcip-token-invalid 無効な GCIP ID トークンが提供されました。
gcip-redirect-invalid リダイレクト URL が無効です。
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 認証 URL がありません。

UI のカスタマイズ

認証ページは、進行状況バーやログアウト ページなどのオプション機能を使用してカスタマイズできます。

進行状況の UI の表示

gcip-iap モジュールがネットワーク タスクを長時間実行しているときに、進行状況を示すカスタム UI をユーザーに表示するには、showProgressBar()hideProgressBar() を実装します。

ユーザーのログアウト

同じ認証 URL を共有するすべてのセッションからのログアウトをユーザーに許可することもできます。

ユーザーがログアウトした後、リダイレクト先の URL が存在しないことがあります。これは、通常、ログインページに関連付けられたすべてのテナントからユーザーがログアウトした場合に発生します。この場合、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 が表示されます。

次のステップ