FirebaseUI を使用したログインページの作成

Identity-Aware Proxy(IAP)で外部 ID を使用するには、ログインページをアプリ側で用意する必要があります。IAP は、ユーザーがセキュリティで保護されたリソースにアクセスする前に、このページにリダイレクトして認証します。

この記事では、オープンソースの JavaScript ライブラリである FirebaseUI で認証ページを作成する方法について説明します。FirebaseUI には、ボイラープレート コードを減らせるような、カスタマイズ可能な要素が用意されています。また、さまざまな ID プロバイダでユーザーのログイン フローを処理します。

すぐに開始できるように、IAP で UI をホストしましょう。これにより、追加のコードを記述することなく、外部 ID を試すことができます。より高度なシナリオでは、独自のログインページをゼロから作成することもできます。このオプションはより複雑ですが、認証フローとユーザー エクスペリエンスを完全に制御できます。

始める前に

外部 ID を有効化し、設定時に [独自に UI を用意する] オプションを選択します。

ライブラリのインストール

gcip-iapfirebasefirebaseui ライブラリをインストールします。gcip-iap モジュールは、アプリ、IAP、Identity Platform 間の通信を抽象化します。firebase ライブラリと firebaseui ライブラリは、認証 UI の構成要素を提供します。

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

CDN では gcip-iap モジュールを使用できません。

次に、ソースファイルで 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 以降

バージョン v1.0.0 以降、gcip-iap には firebase v9 以降とのピア依存関係が必要です。gcip-iap v1.0.0 以降に移行する場合は、次の手順を実行します。

  • package.json ファイルの firebase バージョンと firebaseui バージョンをそれぞれ v9.6.0 以降と v6.0.0 以降に更新します。
  • firebase の import ステートメントを次のように更新します。
// 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
  }
}

ほとんどの場合、1 つの API キーを指定します。ただし、1 つの認証 URL を複数のプロジェクトで使用する場合は、複数の 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 固有のフィールドがあります。これらのフィールドの詳細については、次のセクションで説明します。

テナント選択モードの選択

ユーザーは、オプション モードまたは ID モードのいずれかでテナントを選択できます。

オプション モードの場合、ユーザーはリストからテナントを選択し、ユーザー名とパスワードを入力します。ID モードの場合、ユーザーはまず自分のメールアドレスを入力します。その後、メールアドレスのドメインと一致する ID プロバイダが設定されている最初のテナントが自動的に選択されます。

オプション モードを使用するには、displayModeoptionFirst に設定します。次に、テナントのボタンごとに構成情報(displayNamebuttonColoriconUrl など)を入力します。 オプションの fullLabel を使用して、表示名だけでなく、ボタンラベル全体をオーバーライドすることもできます。

次の例では、オプション モードを使用するようにテナントが構成されています。

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

ID モードを使用する場合は、各ログイン オプションに hd フィールドを指定し、サポート対象のドメインを定義する必要があります。ここには正規表現(例: /@example\.com$/)またはドメイン文字列(例:example.com)を指定できます。

次のコードでは、ID モードを使用するようにテナントを構成しています。

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

即時リダイレクトの有効化

アプリが 1 つの ID プロバイダのみをサポートしている場合、immediateFederatedRedirecttrue に設定すると、ログイン UI をスキップしてユーザーをプロバイダに直接リダイレクトできます。

コールバックの設定

構成オブジェクトには、認証フローのさまざまな時点で呼び出される一連のコールバックが含まれています。これにより、UI をさらにカスタマイズできます。次のフックを使用できます。

selectTenantUiShown() テナント選択の UI が表示されたときにトリガーされます。UI のタイトルやテーマをカスタマイズする場合に使用します。
signInUiShown(tenantId) テナントが選択され、認証情報の入力用 UI が表示されたときにトリガーされます。UI のタイトルやテーマをカスタマイズする場合に使用します。
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. UI をレンダリングする 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. HTML コンテナにレンダリングする FirebaseUiHandler インスタンスを作成し、作成した config 要素をこのインスタンスに渡します。

    const configs = {
      // ...
    }
    const handler = new firebaseui.auth.FirebaseUiHandler(
      '#firebaseui-auth-container', configs);
    
  3. 新しい Authentication インスタンスを作成し、そのインスタンスにハンドラを渡して start() を呼び出します。

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

アプリケーションをデプロイして、認証ページに移動します。テナントとプロバイダを含むログイン UI が表示されます。

次のステップ