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

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

この記事では、認証ページを最初から作成する方法について説明します。この認証ページを自分で作成するのは手間がかかりますが、認証フローとユーザー エクスペリエンスを完全に制御できます。

UI を完全にカスタマイズする必要がない場合は、IAP でログインページをホストするか、代わりに FirebaseUI を使用してコードを簡素化します。

始める前に

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

gcip-iap ライブラリのインストール

gcip-iap NPM モジュールは、アプリケーション、IAP、Identity Platform 間の通信を抽象化します。

ライブラリを使用することを強くおすすめします。UI と IAP 間の基盤となる通信を気にすることなく、認証フロー全体をカスタマイズできます。

次のように、ライブラリを依存関係として含めます。

// 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 v0.1.4 以前を対象としています。

バージョン 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 gcip-iap module.
import * as ciap from 'gcip-iap';

AuthenticationHandler の実装

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;
}

UI をカスタマイズするには、インターフェースを実装するカスタムクラスを作成する必要があります。使用する JavaScript フレームワークに制限はありません。ここでは、各メソッドのビルド方法について詳しく説明します。

テナントの選択

マルチテナンシーを使用する場合は、ユーザーを認証する前にテナントを選択する必要があります。テナントが 1 つだけの場合またはプロジェクト レベルの認証を使用している場合は、この手順をスキップできます。

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

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

電話番号、カスタム、匿名による認証はマルチテナントに対応していません。

テナントを選択するために、ライブラリが selectTenant() コールバックを呼び出します。このメソッドを実装すると、プログラムでテナントを選択できます。また、UI を表示して、ユーザーが選択できるようにすることもできます。

プログラムでテナントを選択する場合、現在のコンテキストを使用します。Authentication クラスの getOriginalURL() メソッドは、認証を行う前に、ユーザーがアクセスしていた URL を返します。これにより、関連するテナントのリストから一致するものを探すことができます。

// 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);
  });
}

UI を表示する場合は、さまざまな方法でプロバイダを指定できます。たとえば、テナントの一覧を表示し、ユーザーに選択させる方法があります。また、ユーザーにメールアドレスを入力させて、そのドメインから一致するものを探すこともできます。

// 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,
          });
        });
  });
}

どの方法でも、あとで認証フローを完了するときに、返された SelectedTenantInfo オブジェクトを使用します。これには、選択したテナントの ID、プロバイダ ID、ユーザーが入力したメールが含まれます。

Auth オブジェクトの取得

プロバイダを取得したら、Auth オブジェクトの取得方法を考える必要があります。指定された API キーとテナント ID に対応する firebase.auth.Auth インスタンスを返すように、getAuth() コールバックを実装します。テナント ID が指定されていない場合は、プロジェクト レベルの ID プロバイダを使用します。

getAuth() は、指定された構成に対応するユーザーの保存場所を追跡するために使用されます。また、認証済みのユーザーの Identity Platform ID トークンを自動的に更新できるので、ユーザーが認証情報を再入力する必要もなくなります。

異なるテナントで複数の IAP リソースを使用する場合は、それぞれに固有の Auth インスタンスを使用することをおすすめします。これにより、構成が異なる複数のリソースで同じ認証ページを使用できます。また、現在のユーザーがログアウトしなくても、他のユーザーが同時にログインできるようになります。

次に、getAuth() の実装方法の例を示します。

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. This makes it possible to have
    // multiple users 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;
}

ユーザーのログイン

ログインを処理するには、startSignIn() コールバックを実装します。これは、ユーザーが認証を行うための UI を表示し、完了するとログイン ユーザーの UserCredential を返します。

マルチテナント環境で SelectedTenantInfo が提供されている場合は、これにより使用可能な認証方法を決めることができます。この変数には、selectTenant() コールバックから返される同じ情報が格納されます。

次の例では、メールアドレスとパスワードを使用して既存のユーザーに startSignIn() を実装しています。

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show 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 user for email and password.
      // 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 error message.
        });
    });
  });
}

ポップアップやリダイレクトを使用して、SAML や OIDC などの連携プロバイダでユーザーにログインさせることもできます。

startSignIn(auth, selectedTenantInfo) {
  // Show UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // 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 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);
  });
}

一部の OAuth プロバイダではログインのヒントを渡すことができます。

startSignIn(auth, selectedTenantInfo) {
  // Show 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.
    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 error message.
       });
    });
}

ユーザー認証の詳細については、Identity Platform のドキュメントをご覧ください。

ユーザーの処理

オプションの processUser() メソッドを使用すると、IAP リソースにリダイレクトする前に現在ログインしているユーザーを変更できます。これは次のような場合に使用します。

  • 他のプロバイダにリンクする。
  • ユーザーのプロフィールを更新する。
  • 登録後にユーザーに追加データを要求する。
  • signInWithRedirect() を呼び出した後に、getRedirectResult() から返された OAuth アクセス トークンを処理する。

次に、processUser() の実装例を示します。

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask 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 トークン クレームに反映されるユーザーを変更するには、トークンを強制的に更新する必要があります。

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

進行状況の UI の表示

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

エラー処理

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 がありません。

ユーザーのログアウト

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

ユーザーがログアウトした後、リダイレクト先の URL が存在しないことがあります(これは、通常、ログインページに関連付けられたすべてのテナントからユーザーがログアウトした場合に発生します)。この場合、completeSignOut() コールバックを実装し、ユーザーが正常にログアウトしたことを通知するメッセージを表示します。この処理を行わないと、空白のページが表示されます。

カスタム UI の使用

AuthenticationHandler を実装するクラスを作成したら、そのクラスを使用して新しい Authentication インスタンスを作成し、開始します。

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

アプリケーションをデプロイして、認証ページに移動します。カスタム ログインの UI が表示されます。

次のステップ