TL;DR 本書では、Firebase および Firestore でリアルタイム Web チャットを作る方法を学びます。さらに、認証システムとして Firebase アプリで Auth0 を使う方法(そしてそれを使用する理由)も学びます。これらテクノロジーと統合の手始めとして、確実にメッセージを Firestore に保存し(Firebase が提供するリアルタイムのデータベース)、ユーザーが Auth0 を通して認証できる簡単なリアルタイム Web チャットを構築します。必要であれば、この GitHub リポジトリにある記事で作成した最後のコードをご覧ください

「Firebase および Firestore でリアルタイム Web チャットを作る方法を学びましょう。」

前提条件

以下のステップに従うには、Node.js および JavaScript についての基本的な理解を深めてください。サーバー側には Node.js を、プロジェクトのパッケージ マネージャーには NPM を使用します。よって、両方のソフトウェアをローカルにインストールする必要があります。これらのツールをインストールしていなければ、このリソースを確認してください(NPM には Node.js が含まれています)

Firebase とは何か?

Firebase はプラットフォームで、Web およびモバイル アプリケーションを素早く作成できます。このプラットフォームは特定の機能を提供するためにプロジェクトに簡単に統合できるサービスを提供します。これらサービスのひとつは Cloud Firestore で、Google が提供するフレキシブル、スケーラブル、リアルタイムな NoSQL データベースです。本書では Firestore を使ってチャットアプリケーションのリアルタイム データベースを設定します。

Firebase と Auth0 を一緒に使用するのはなぜか?

Firebase には認証機能が含まれていることが良い点です。ただし、前述したように、認証方法として Firebase インスタンスで Auth0 を構成していきます。これを念頭に、Firebase を使って Auth0 のような外部認証プロバイダーを使う必要性について疑問に思われているかもしれません。

Auth0 を使って Firebase サービスを安全にすることは、必ずしも Firebase 認証を使用しないということではありません。実際のところ、Firebase はカスタム トークンを使うカスタム認証のアプローチを提供しており、これによって Firebase を安全にするために Firebase がすでに提供するビルトイン認証アプローチを使うよりもご希望の ID プロバイダーを使うことが可能になります。では、Firebase のビルトイン認証方法よりも Auth0 を使ったカスタム認証アプローチを使うのはなぜでしょうか?この疑問に対する回答は複数あります。

まとめると、アプリケーションが信頼性の高い複雑な認証方法を必要とするのであれば(大抵は必要とする)、カスタム認証アプローチを使用すべきです。ありがたいことに、Firebase はその他の認証ソリューションをそのサービスに統合する方法を提供しています。これについての詳細は本書で説明しています。

「Auth0 を Firebase アプリケーションでカスタム認証プロバイダーとして使用すると、さらに信頼性の高いアプリにすることができます。」

どのようにして Firebase と Auth0 を連携させるか?

Firebase と Auth0 を連携させることについては後ほど説明しますが、とても簡単です。その流れ全体は Auth0 を使ってアプリケーション(ネイティブアプリ、Web アプリ、SPA などにかかわらず)を安全にすることから始まります。ユーザーが認証プロセスの一貫として Auth0 を通して認証するとき、アプリは Auth0 で安全にされるバックエンド API にリクエスを発行するために使用するバックトークン(idTokens および/または accessTokens)を取得します。これら API はリクエストを処理している間、これらのトークンを取得し、これらが信頼されたプロバイダーが発行したものか(この場合 Auth0)、有効期限が切れていないかを検証します。これらのトークンが有効であれば、API はそれに応じてリクエストを実装します。

上記の流れはアプリケーションを Auth0 で安全にしたときに通常発生するステップです。では、Firebase をこのレシピに追加するには、バックエンド API (Auth0 で安全になっている)にエンドポイントを作成してカスタム Firebase トークンを生成します。それから、バックエントがこれらトークンの作成が終わったら、クライアント アプリがそれらを返して、それらを使ってFirebase への認証に使用します。

ここでのマジックは、バックエンド API とフロントエンド クライアントが Auth0 によって安全になっているので、ユーザーは Auth0 を通してまず認証されたのであれば、カスタム Firebase トークンのみを取得できます。この操作にご関心がある方は、後ほどこの流れ全体を実装しますので、ご心配はありません。

Firebase を設定する

Firebase を使用するためには、Firebase アカウントにサインアップする必要があります。それが終わったら、次にやることは Firebase プロジェクトを作成することです。このためにはFirebase コンソールに移動してプロジェクトの追加 をクリックします。これをクリックすると、Firebase はプロジェクトの名前 (「Firestore チャットアプリ」のような名前)を定義するフォームが表示されますので、2つのチェックボックスをティックします。それが終わったら、プロジェクトの作成 ボタンをクリックします。

数秒後、Firebase はプロジェクトの作成を終え、プロジェクトの概要 ページにリダイレクトされます。そこから </> のようなボタンをクリックすると、Firebase プロパティでポップアップが表示されます。このポップアップから apiKeyauthDomain、および projectId 値をこのポップアップから複写しそれらをどこかに保存します。 これらはクライアントアプリで Firebase を構成しているときに後ほど使用します。

Copying your Firebase properties.

ここで、Firestore を構成していきます。上述したように、これから構成するクライアント アプリケーションは Firestore を使ってチャット メッセージを保存します。よって、新規データベースを Firebase コンソールに作る必要があります。そのためには、縦メニューにあるデータベース オプションをクリックします。それから、データベースの作成 ボタンをクリックし、テストモードで開始 オプションを選択し、有効 をクリックします。数秒たったら、Firestore データベースを使用する用意ができました。

それから、Firestore データベースを安全にするには、新規 Firestore データベースの規則 セクションに移動して既定の規則と次を交換します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /messages/{message} {
      allow read: if true;
      allow write: if request.auth.uid != null;
    }
  }
}

基本的に、この規則は誰もがデータベースから読み取ることができるが(読み取りを許可:真実であれば)、認証ユーザーのみがそれを書き取る(書き取りを許可:request.auth.uid != null であれば)ことができることを提示します。この規則を設置したので、発行 ボタンを押して即座に規則を有効にします。

Adding a security rule to your Firestore database.

サービス アカウント秘密キーを生成する

カスタム認証アプローチを使用しているので、Firebase で認証に使用するカスタム トークンを作成する必要があります。Firebase はこれらカスタムトークンを作成するのに役立つ管理者 SDK を提供しますが、この SDK が機能するには、サービスアカウント に関する資格情報(ファイル形態)を与える必要があります。このアカウントを作成してこれら資格情報(このファイル)を取得するには、プロジェクトの概要 の横にある小さな歯車のアイコンをクリックし、それからプロジェクト設定 をクリックします。

Accessing your Firebase project settings.

それをクリックしたら、Firebase はプロジェクトの設定ページにリダイレクトします。それがロードしたら、サービス アカウント タブに移動して Firebase 管理者 SDK オプションを選択します。それから、新しい秘密キーの生成 ボタンをクリックします。すぐに、Firebase はサービスアカウントの資格情報と共に JSON ファイルを送信します。現時点では、このファイルを安全な場所に保存します。のちほど、この資格情報が必要になります。

注: サービスアカウントの秘密キーを含む JSON ファイルは 非常に機密性の高いもの なので、GitHub のような公共リポジトリとは離して保存してください。

Auth0 を設定する

同様に、Auth0 を使用するには、アカウントが必要です。アカウントがない方は、ここから無料でサインアップ してください。

アカウントの作成が終わったらダッシュボードのアプリケーション セクションに移動し、アプリケーションの作成 をクリックします。このボタンをクリックすると、Auth0 がポップアップを表示し、ここでアプリケーションの名前 と(再び、「Firestore チャットアプリ」の名前を使えます)アプリのタイプを伝えなければなりません。リアルタイム チャットアプリ(再読み込みに依存しないアプリ)を構築するとき、シングルページ Web アプリケーション を選択する必要があります。それから、作成 ボタンをクリックすると、Auth0 は新規アプリケーションのクイック スタート セクションにリダイレクトします。そこから、設定 タブをクリックし、ある特定フィールドを変更します。

設定 タブに移動したら、許可されたコールバック URL フィールドを探し、http://localhost:3001/ をそれに加えます。このフィールドは、認証後、ユーザーが構成するもの(この場合はhttp://localhost:3001/)にリダイレクトされるように Auth0 に指示します。それだけです。これは Auth0 によって実装されたセキュリティ対策で、他のアプリがユーザーのアプリに生成されたトークンを取得しないことを保証します。

このフィールドを満たしたら、このページの下部にある変更の保存 ボタンをクリックして構成を更新しますが、後ほどこのページから情報をコピーするので、ここは閉じないでください。

Web サーバーをスキャフォールディングする

Auth0 および Firebase の設定が終わったので、次にやることはアプリケーションのディレクトリを作成することです。コンピューターを起動して次のコマンドを実行します。

# プロジェクトに移動します
cd firestore-web-chat

# それを NPM ペッケージとして初期化します
npm init -y

このコマンドは package.json ファイルを初期化し、すべてのプロジェクトの依存関係を追跡するのに役立ちます。このファイルにはアプリケーションについての有効な情報も含みます。

ここで、バックエンドの依存関係をインストールする必要があります。サーバー側にアプリケーションの特定部分を構築するのにパッケージの一部を使用します。以下のコマンドを実行して次のパッケージをインストールします。

npm install express cors express-jwt jwks-rsa firebase-admin

以下はこれらパッケージが何をし、それがどのように役立つかについての簡単な要約です。

  • expressExpress は Node.js フレームワークで、Node.js を使って Web アプリケーションの構築が簡単になります。
  • cors:このパッケージは Express ミドルウェアとして機能し、サーバー上のクロス オリジン リソース共有を有効にします。
  • express-jwtJSON Web Tokens (JWT) を使って HTTP リクエストを認証するにはこのパッケージを使います。このパッケージは HTTP リクエストの Authorization ヘッダーに送信したトークン(JWT)を抽出し、基本的にこのトークンを検証しようとする Express ミドルウェアです。
  • jwks-rsa:このパッケージはこれらトークンを署名するのに使う JWKS (JSON Web Key Set) エンドポイントからそのキーを取得するのに使います。
  • firebase-admin:このパッケージはサーバーなど特権のある環境から Firebase サービスとの対話を可能にします。今回は特にカスタム認証トークンを作るのにこのパッケージを使用します。

カスタム認証トークンを作る

これでバックエンド サーバーのスキャフォールディングが終わったので、プロジェクトのコーディングを始めることができます。そこで、プロジェクト ルート ディレクトリに src というフォルダーを作ります。このフォルダーにはすべてのアプリケーション コードが含まれます(サーバー側とクライアント側の両方)。ここで、この新しいフォルダー内に server.js というファイルを作り、次のコードをそれに挿入します。

// src/server.js
const express = require('express');
const cors = require('cors');
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
const path = require('path');

const app = express();
app.use(cors());
app.use('/', express.static(path.join(__dirname, 'public')));

const jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
  }),
  audience: process.env.AUTH0_API_AUDIENCE,
  issuer: `https://${process.env.AUTH0_DOMAIN}/`,
  algorithm: 'RS256'
});

上記のコード スニペットで最初に実行したのは、以前インストールしたパッケージをインポートすることでした。その後、express を初期化し、サーバーが異なるオリジンからのリクエストを受け入れるために cors を構成しました。それから、その直後に2つの重要なことが発生します。

  1. Express サーバーに、ユーザーが使用できる静的ファイルとして public というディレクトリの下のファイルを処理するように伝えます(このディレクトリは間もなく作ります)。

  2. クライアントから送信された JSON Web Token を検証する jwtCheckexpress-jwt の助けを得て)というミドルウェアを作りました。

さらに詳しくは、クライアントがサーバー上の特定ルートにリクエストするとき、トークン(Auth0 の署名済み)と共に Authorization ヘッダーに送信されます。jwtCheck ミドルウェアはこのトークンを展開し、有効かを確認します。このトークンが無効な場合、または危害を受けていた場合、即座に拒否されます。そうでなければ、このトークンはデコードされ、ペイロードは次のミドルウェアに送信されます。

ここで、このミドルウェアはどのようにしてトークンの有効性を確証するのか、疑問に思われるかもしれません。特定の構成プロパティを使って、このミドルウェアがトークンの検証を正確に知らせるように構成します。

  • secret:これは公開キーで、トークンを確認/検証するために使用します。クライアントから送信されたトークンは Auth0 の署名済みトークンだと予測されます。Auth0 は、Auth0 発行のトークンを署名するために使用される暗号化キーを表す JWK (JSON Web Key) を含み、JSON ファイルにポイントする各テナントのエンドポイント(ユーザーのものを含む)を公開します。この JWK はトークンの信頼性を検証するために使用される公開キーも含みます。jwks-rsa: パッケージはテナントに Auth0 によって公開されたエンドポイントからこのキーを取得するために使用します。
  • audience:対象ユーザーは Auth0 clientID を使って JWT の受信者を識別します。
  • issuer:これは JWT を発行する当事者を一意に識別する URI です。Auth0 が発行した ID トークンの場合はこれは Auth0 アプリケーションの domain です(例:blog-samples.auth0.com)。
  • algorithm:これは Auth0 が JWT を署名するために使用したアルゴリズムを示します。RS256 はここでは、トークンを署名するために使用されます。このアプローチは秘密キーは非対称アルゴリズムを使用しています。つまり 秘密キーは JWT を署名するために使用し、公開キーはその署名を検証するために使用することを意味します。このアルゴリズムに関する詳細はこの記事をご覧ください。

これで、どのように Auth0 を使って Express アプリを安全にするかを学んだので、src ディレクトリに firebase という新しいフォルダーを作ります。このフォルダーには、以前 Firebase からダウンロードした JSON ファイルを配置します。これは Firebase サービス アカウントの資格情報を含むファイルです。この JSON ファイルがレポジトリにコミットされていないように、同じ firebase フォルダーに .gitignore ファイルを作り、生成した JSON ファイルの名前をそれに加え、読みやすくするために JSON ファイルを firebase-key.json に名前を変更します。

# src/firebase/.gitignore
firebase-key.json

では、次にクライアントが送信したトークンを検証するために構成した認証ミドルウェアを使う Express ルートを設定します。クライアントの Firestore との通信を可能にする カスタム Firebase トークンを作るために Firebase Admin SDK を使います。次のコードを server.jsファイルの下部に貼り付けます。

// src/server.js

// ... 残りはそのままにします ...

const serviceAccount = require('./firebase/firebase-key');

firebaseAdmin.initializeApp({
  credential: firebaseAdmin.credential.cert(serviceAccount),
  databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
});

app.get('/firebase', jwtCheck, async (req, res) => {
  const {sub: uid} = req.user;

  try {
    const firebaseToken = await firebaseAdmin.auth().createCustomToken(uid);
    res.json({firebaseToken});
  } catch (err) {
    res.status(500).send({
      message: 'Firebase トークンを取得するときにエラーが発生しました。',
      error: err
    });
  }
});

app.listen(3001, () => console.log('Server running on localhost:3001'));

上記でまずしたのは、Firebase からダウンロードした資格情報で Firebase Admin SDK を初期化しました。その後、/firebase の下に Express ルートを作成したので、クライアントは Firestore と通信するためにカスタムトークンを取得できます。Auth0 を通して認証されたユーザーのみが確実にカスタム Firebase トークンを取得できるように、jwtCheck を /firebase route にプラグインしました。クライアントからこのルータへの各リクエストで、ミドルウェアはトークンを抽出し、それを検証します。そのトークンが有効であれば、トークンが運ぶペイロードは req.user オブジェクトに読み込まれ、次のミドルウェアに送信されます。そうでなければ、そのト-クンが無効な場合、クライアントにエラーメッセージがスローされ、その後のアクセスは拒否されます。

req.user オブジェクト上のペイロードは sub というプロパティを含み、各ユーザーを一意に識別するために使用されます。Firebase Admin SDK はカスタムトークンを作るためにこの一意の識別子を使い、そのカスタムトークンはクライアントに戻されます。それから、クライアントはそのトークンを使って Firebase で認証/検証します。サーバー側はこれだけです。次に、クライアント側のアプリケーションを設定します。

上記の機能には async キーワードでプレフィックスが付いていることが分かります。これは Promise で非同期操作が行われることを表すために使用します。 非同期/待機について馴染みのない方はこちらをご覧ください

ユーザー インターフェイスを構築する

本章では、簡単なリアルタイム チャット アプリケーションであるクライアント アプリケーションを構築します。まず、しなければならないことは UI を設定することです。ですから、src ディレクトリに public というフォルダーを作ります。このディレクトリにはすべてのクライアント側のコードが含まれます。その後、index.html というファイルを作り、public フォルダーに入れます。それが終わったら、次のコードを index.html ファイルにコピー&ペーストします。

<!doctype html>
<html lang="en" class="h-100">
<head>
  <!-- 必須メタタグ -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
        integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

  <title>Auth0 および Firebase チャット アプリ</title>
</head>
<body class="h-100">

<div class="container-fluid d-flex flex-column h-100">
  <div class="row bg-primary p-2 text-white" style="z-index: 10000; min-height: 60px;">
    <div class="col-12 p-2">
      <div id="profile" class="font-weight-bold"></div>
      <button id="sign-in" type="button" class="btn">Sign In</button>
      <button id="sign-out" type="button" class="btn">Sign Out</button>
    </div>
  </div>
  <div class="row flex-fill">
    <div id="chat-area" class="col-12 d-flex flex-column-reverse" style="overflow-y: auto;">
    </div>
  </div>
  <div class="row bg-dark p-2" style="min-height: 55px;">
    <div class="col-12">
      <input type="text" class="form-control" id="message" aria-describedby="message-help"
             placeholder="Enter your message" disabled>
    </div>
  </div>
</div>

<script src="https://www.gstatic.com/firebasejs/5.3.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.3.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.3.0/firebase-firestore.js"></script>
<script src="https://cdn.auth0.com/js/auth0/9.7.3/auth0.min.js"></script>
<script src="/app/auth0.js"></script>
<script src="/app/firebase.js"></script>
<script src="/app/index.js"></script>
</body>
</html>

上記には難しいものはありません。チャット アプリケーションのスタイルを設定しBootstrapの助けを得て)、Auth0 および Firebase(auth0.min.js および3つの firebase-*.js スクリプト)が提供する外部ライブラリを指すスクリプト タグを追加しているだけです。これらライブラリは以前作成した Auth0 および Firebase アプリケーション/インスタンスを初期化し、対話できるようにします。

また、このファイルは次のようにアプリの UI の主な要素を定義します。

  • sign-in:ユーザーの認証を有効にするボタン。
  • sign-out:ユーザーのサインアウトを有効にするボタン。
  • chat-area:リアルタイム メッセージが表示される領域。
  • message:ユーザーがメッセージをタイプする入力テキスト。

最後に、ローカル ファイルにポイントする追加のスクリプト タグを追加します(/app/auth0.js/app/firebase.js、および /app/index.js)。これらファイルは次のセクションで作成します。

クライアントに Auth0 を構成する

ここでは、Auth0 との通信を構築するクライアント側アプリを可能にするファイルを作成します(Auth0 を認証サーバーとして使用するなど)。そのためには、app という別のディレクトリを /src/public の中に作り、auth0.js というファイルをこの新しいディレクトリに作ります。ここで Auth0 を初期化し構成します。ここで、コードをこのファイルに貼り付けます。

// ./src/public/app/auth0.js

let _auth0Client = null;
let _idToken = null;
let _profile = null;

class Auth0Client {
  constructor() {
    _auth0Client = new auth0.WebAuth({
      domain: 'YOUR_APP_DOMAIN',
      audience: 'https://YOUR_APP_DOMAIN/userinfo',
      clientID: 'YOUR_APP_CLIENTID',
      redirectUri: 'http://localhost:3001/',
      responseType: 'token id_token',
      scope: 'openid profile'
    });
  }

  getIdToken() {
    return _idToken;
  }

  getProfile() {
    return _profile;
  }

  handleCallback() {
    return new Promise((resolve, reject) => {
      _auth0Client.parseHash(async (err, authResult) => {
        window.location.hash = '';
        if (err) return reject(err);

        if (!authResult || !authResult.idToken) {
          // not an authentication request
          return resolve(false);
        }
        _idToken = authResult.idToken;
        _profile = authResult.idTokenPayload;

        return resolve(true);
      });
    });
  }

  signIn() {
    _auth0Client.authorize();
  }

  signOut() {
    _idToken = null;
    _profile = null;
  }
}

const auth0Client = new Auth0Client();

ご覧のように、Auth0Client というクラスを定義し、このクラスのインスタンスである auth0Client というグローバル定数を定義します。このクラス(およびそのインスタンス)は認証フローを処理する特定のメソッドを実装します。

  • constructor:クラスのコンストラクターは auth0-js ライブラリを独自の Auth0 資格情報で構成します。
  • signIn:このメソッドはユーザーを Auth0 にリダイレクトし認証メソッドを選択できるようにして、認証プロセスを初期化します。
  • signOut:このメソッドは _idToken および _profile をクリーニングして現在のユーザー セッションを取り除きます。
  • handleCallback:このメソッドはトークンがないか、現在のページの URL ハッシュを確認します(これは実際、parseHash で行われます)。ユーザー認証が終わったら、Auth0 は URL のハッシュフラグメント上のその情報(トークン)と一緒にそれをアプリに戻します。これらトークンをフェッチするには、このメソッドを呼び出し、それに応じてクライアント側セッションを設定します(例: _idToken および _profile 変数を設定する)。
  • getIdToken および getProfile:これらメソッドはアプリがユーザー情報を使用できるようにします。

注: すべての YOUR_APP_DOMAIN プレースホルダーの発生および YOUR_APP_CLIENTID プレースホルダーの発生と独自の Auth0 プロパティを置き換えます。 YOUR_APP_DOMAIN を置き換えるには、以前に作成した Auth0 アプリケーションのドメイン フィールドに表示の値をコピーします(例:blog-samples.auth0.com)。YOUR_APP_CLIENTID を置き換えるには、Auth0 アプリケーションのクライアント ID フィールドを使います。

Copying Auth0 domain and client id from your Auth0 Application.

クライアントに Firebase を構成する

ここまでできたので、次に行うことは Firebase をクライアント側アプリに構成することです。そのためには、firebase.js という新しいファイルを app ディレクトリーに作ります。それから、次のコードをこのファイルに追加します。

// ./src/public/app/firebase.js

let _messagesDb = null;

class Firebase {
  constructor() {
    firebase.initializeApp({
      apiKey: 'YOUR_PROJECT_APIKEY',
      authDomain: 'YOUR_PROJECT_AUTHDOMAIN',
      projectId: 'YOUR_PROJECT_ID',
    });

    // initialize Firestore through Firebase
    _messagesDb = firebase.firestore();

    // disable deprecated features
    _messagesDb.settings({
      timestampsInSnapshots: true
    });
  }

  async addMessage(message) {
    const createdAt = new Date();
    const author = firebase.auth().currentUser.displayName;
    return await _messagesDb.collection('messages').add({
      author,
      createdAt,
      message,
    });
  }

  getCurrentUser() {
    return firebase.auth().currentUser;
  }

  async updateProfile(profile) {
    if (!firebase.auth().currentUser) return;
    await firebase.auth().currentUser.updateProfile({
      displayName: profile.name,
      photoURL: profile.picture,
    });
  }

  async signOut() {
    await firebase.auth().signOut();
  }
}

前のセクションで実行したように、クラスを作成してこのファイルでサードパーティ サービスと対話するのに役立つクラスを作成します(この場合、Auth0 ではなく Firebase)。この新しいスクリプトは次のように機能します。

  • constructor:コンストラクター機能で Firebase および Firestore の両方を初期化します。
  • addMessage:クライアント側アプリを有効にして Firestore データベースにチャット メッセージを追加するためにこのメソッドを定義します。ユーザーがチャット メッセージを送信するときはいつでもアプリは関数を呼び出して引数としてメッセージをパスします。ご覧のように、メッサ―ジをデータベースで並べ替えるよりも createdAt や author のようなその他のプロパティ をメッセージに追加します。author 変数を定義するこの displayName が来る場所から少し学びます。
  • signOut:このメソッドは Firebase セッションを終了します。
  • updateProfile:このメソッドは現在サインインしたユーザーのプロファイル情報でパラメータを取得し、それを使ってユーザーの Firebase プロファイルを更新します。
  • getCurrentUser:このメッソドは現在のユーザーの詳細を返します。

注: YOUR_PROJECT_APIKEYYOUR_PROJECT_AUTHDOMAIN、および YOUR_PROJECT_ID を独自の Firebase 値と置き換えます。これらプレースホルダーはそれぞれ前にコピーした apiKeyauthDomain、および projectId 値と置き換えることができます。

Copying your Firebase properties.

ここで、このスクリプトを完了するには、次のメソッドを Firebase クラスに追加します。

let _messagesDb = null;

class Firebase {
  // ... constructor and methods defined above ...

  setAuthStateListener(listener) {
    firebase.auth().onAuthStateChanged(listener);
  }

  setMessagesListener(listener) {
    _messagesDb.collection('messages').orderBy('createdAt', 'desc').limit(10).onSnapshot(listener);
  }

  async setToken(token) {
    await firebase.auth().signInWithCustomToken(token);
  }
}

const firebaseClient = new Firebase();

注: 最後のラインを加え忘れないでください(const firebaseClient = new Firebase();)。

これら新しいメソッドは次の機能をこのクラスに追加します。

  • setAuthListener:このメソッドは、 Firebase の認証状態が変わるときはいつでも呼び出すリスナーを追加するアプリを有効にします。
  • setMessagesListener:このメソッドは Firestore データベースの messages コレクションにリスナーを追加します。Firebase はこのコレクションが変わるといつでもリアルタイムでこのリスナーを呼び出します。
  • setToken:このメソッドはサーバーに生成するカスタムトークンを受け取り、これらを Firebase で認証するために使用します。

アプリが Firebase で通信するために必要な機能はこれだけです。次に、Auth0 および Firebase の両方を一緒に統合してリアルタイム Web チャットを構築するために、クライアント側アプリの主スクリプトを作ります。

リアルタイム Web チャット UI を実装する

リアルタイム チャット アプリを完成するために最後に必要なことは index.js という新しいファイルを ./src/public/app に作り、次のコードをそれに挿入します。

// ./src/public/app/index.js

const chatArea = document.getElementById('chat-area');
const messageInput = document.getElementById('message');
const profileElement = document.getElementById('profile');
const signInButton = document.getElementById('sign-in');
const signOutButton = document.getElementById('sign-out');

messageInput.addEventListener('keyup', async (event) => {
  if (event.code !== 'Enter') return;
  firebaseClient.addMessage(messageInput.value);
  messageInput.value = '';
});

signInButton.addEventListener('click', async () => {
  auth0Client.signIn();
});

signOutButton.addEventListener('click', async () => {
  auth0Client.signOut();
  firebaseClient.signOut();
  deactivateChat();
});

async function setFirebaseCustomToken() {
  const response = await fetch('http://localhost:3001/firebase', {
    headers: {
      'Authorization': `Bearer ${auth0Client.getIdToken()}`,
    },
  });

  const data = await response.json();
  await firebaseClient.setToken(data.firebaseToken);
  await firebaseClient.updateProfile(auth0Client.getProfile());
  activateChat();
}

function activateChat() {
  const {displayName} = firebase.auth().currentUser;
  profileElement.innerText = `Hello, ${displayName}.`;
  signInButton.style.display = 'none';
  signOutButton.style.display = 'inline-block';
  messageInput.disabled = false;
  firebaseClient.setMessagesListener((querySnapshot) => {
    chatArea.innerHTML = '';
    querySnapshot.forEach((doc) => {
      const messageContainer = document.createElement('div');
      const timestampElement = document.createElement('small');
      const messageElement = document.createElement('p');

      const messageDate = new Date(doc.data().createdAt.seconds * 1000);
      timestampElement.innerText = doc.data().author + ' - ' + messageDate.toISOString().replace('T', ' ').substring(0, 19);
      messageElement.innerText = doc.data().message;
      messageContainer.appendChild(timestampElement);
      messageContainer.appendChild(messageElement);
      messageContainer.className = 'alert alert-secondary';
      chatArea.appendChild(messageContainer);
    });
  });
}

function deactivateChat() {
  profileElement.innerText = '';
  signInButton.style.display = 'inline-block';
  signOutButton.style.display = 'none';
  messageInput.disabled = true;
}

(async () => {
  deactivateChat();

  const loggedInThroughCallback = await auth0Client.handleCallback();
  if (loggedInThroughCallback) await setFirebaseCustomToken();
})();

プロジェクトに追加する新しいスクリプトの主な目標は UI アプリ全体をコントロールし、カスタムトークンで Firebase にサインインすることです。さらに具体的には、このスクリプトは次の UI 要素に参照を作って始めます。

  • chatArea:アプリがメッセージを表示する場合
  • messageInput:ユーザーがメッセージをタイプする場合
  • profileElement:アプリがログインしたユーザーの名前を表示する場合
  • signInButton:これでユーザーの認証を可能にします。
  • signOutButton:これでユーザーのサインアウトを可能にします。

その後、スクリプトは keyUp リスナーを messageInput 要素に追加します。このリスナーはユーザーが押すキーを確認し、Enter キーを押していれば、このリスナーは新しいメッセージを発行するために firebaseClient.addMessage を呼び出します。

スクリプトが次にすることは、イベント リスナーを signInButton と signOutButton に追加してユーザーが認証して必要な時にサインアウトできるようにします。

次に、setFirebaseCustomToken という関数を定義し、呼び出されたときに、Auth0 から取得した idToken を使って AJAX リクエストを http://localhost:3001/firebase に発行します。ここでの目標はバックエンド サーバーからカスタム Firebase トークンを取得することで、アプリが問題なく Firestore データベースと(認証のために)通信できるようにすることです。また、この関数は Auth0 が返したプロファイルで現在のユーザーの Firebase プロファイルを更新します。

これができたので、このスクリプトはさらに2つの関数を追加します。

  • activateChat:この関数は UI 全体をアクティブにして現在のユーザーがメッセージを発行できるようにし、リスナーを Firestore に加えて新しいメッセージが到着したときに、UI が更新されます。
  • deactivateChat:この関数はユーザーがサインアウトしたらチャットをクリアにします。

それから、最後にこのスクリプトがすることは、次の2つをするために IIFE(即時実行関数式)を使用することです。

  1. ユーザーが Auth0 から戻るかどうかを確認するために handleCallback 関数を呼び出します(認証後)。
  2. (ユーザーがサインインしたら)カスタムトークンで Firebase にサインインするために setFirebaseCustomToken 関数を呼び出します。

「リアルタイム Web アプリを Firebase で作ることは簡単で楽しいです。」

リアルタイム Web チャットをテストする

やりました!リアルタイム Web チャットの作成が終わりました。では、それを使ってみましょう。アプリを実行するには、プロジェクト ルート ディレクトリにいることを確認し、次のコマンドを発行します。

# server.js で使用した env 変数を定義します
export AUTH0_DOMAIN=YOUR_APP_DOMAIN
export AUTH0_API_AUDIENCE=YOUR_APP_AUDIENCE

# アプリを実行します
node src/server

注: YOUR_APP_DOMAIN を独自の Auth0 ドメインに、 AUTH0_API_AUDIENCE を Auth0 アプリケーション クライアント id に置き換える必要があります。これら両方の値は以前に定義した auth0.js ファイルにあります(各domain および clientID)。

その後、http://localhost:3001 を Web ブラウザーに開くと、スクリーンに Web チャットが表示されます。それから、サインインをクリックすると、アプリが Auth0 にリダイレクトされて認証してチャットを始めることができます。サインインしたら、Auth0 はアプリにリダイレクトし、メッセージを送り始めることができます(メッセージをタイプして Enter キーを押すだけ)。素晴らしいですね?

Real-time web chat built with Firebase, Firestore, and Auth0.

まとめ

本書では Firebase および Firestore を使ってリアルタイム Web アプリを構築する方法を学びました。そのほかに、カスタム認証システムとして Auth0 を Firebase インスタンスに構成する方法とその理由についても学びました。最後に、Firebase および Auth0 の両方を使う素敵なご自分の Web チャットアプリケーションを作り、モダンでリアルタイムの Web アプリができました。素晴らしいでしょう?