Python 2 は、コミュニティによるサポートを終了しました。Python 2 アプリを Python 3 に移行することをおすすめします。

Firebase を使用した App Engine 上でのユーザーの認証

このチュートリアルでは、Firebase Authentication、App Engine スタンダード環境、Datastore を使用してユーザー認証情報を取得、確認、保存する方法を説明します。

本書では、Firenotes という名前のシンプルなメモ編集アプリケーションの使い方について説明します。このアプリケーションは、ユーザーのメモを個人用のノートブックに保存します。ノートブックはユーザーごとに保存され、各ユーザーの一意の Firebase Authentication ID によって識別されます。このアプリケーションには次のコンポーネントがあります。

  • フロントエンドはログイン ユーザー インターフェースを構成し、Firebase Authentication ID を取得します。また、認証状態の変化を処理し、ユーザーにメモを表示します。

  • FirebaseUI は、認証と UI タスクを簡素化するオープンソースのドロップイン ソリューションです。SDK は、ユーザー ログイン、複数のプロバイダの 1 つのアカウントへのリンク、パスワードの回復などを処理します。認証のおすすめの方法を実装して、スムーズで安全なログイン方式を実現します。

    FirebaseUI
  • バックエンドは、ユーザーの認証状態を検証し、ユーザーのプロフィール情報およびメモを返します。

アプリケーションは NDB クライアント ライブラリを使用してユーザー認証情報を Datastore に保存しますが、認証情報は任意のデータベースに保存できます。

次の図は、フロントエンドとバックエンドが相互に通信する方法、およびユーザー認証情報が Firebase からデータベースにどのように移動するかを示しています。
リクエストのパスとユーザー認証情報

Firenotes は、Flask ウェブ アプリケーション フレームワークをベースにしています。サンプルアプリが Flask を使用しているのは、単純で使いやすいからです。ただし、ここで説明する概念やテクノロジーは、使用するフレームワークに関係なく、適用可能です。

目標

このチュートリアルの目標は次のとおりです。

  • Firebase Authentication ユーザー インターフェースを構成する。
  • Firebase ID トークンを取得し、サーバー側の認証を使用して検証する。
  • ユーザー認証情報と関連データを Datastore に保存する。
  • NDB クライアント ライブラリを使用してデータベースに問い合わせる。
  • アプリを App Engine にデプロイする。

費用

このチュートリアルでは、Google Cloud の課金対象となる以下のコンポーネントを使用します。

  • データストア

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを出すことができます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. GitPython 2.7virtualenv をインストールします。 Python の最新バージョンのインストールなど、Python 開発環境の設定について詳しくは、Google Cloud の Python 開発環境のセットアップをご覧ください。
  2. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  3. Cloud Console のプロジェクト セレクタページで、Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  4. Cloud SDK をインストールして初期化します。

すでに SDK をインストールして別のプロジェクトに初期化してある場合は、gcloud プロジェクトを Firenotes に使用している App Engine プロジェクト ID に設定します。gcloud ツールを使用してプロジェクトを更新するための個別のコマンドについては、Cloud SDK 構成の管理をご覧ください。

サンプルアプリのクローン作成

サンプルをローカルマシンにダウンロードするには、次のようにします。

  1. サンプル アプリケーション レポジトリをローカルマシンにクローン作成します。

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    または、zip ファイルとしてサンプルをダウンロードして解凍します。

  2. サンプルコードが含まれるディレクトリに移動します。

    cd python-docs-samples/appengine/standard/firebase/firenotes
    
    FirebaseUI を設定して ID プロバイダを有効にするには、次のようにします。

  3. Firebase をアプリに追加する手順は次のとおりです。

    1. Firebase コンソールで Firebase プロジェクトを作成します。
      • 既存の Firebase プロジェクトがない場合は、[プロジェクトを追加] をクリックし、既存の Google Cloud プロジェクト名または新しいプロジェクト名を入力します。
      • 既存の Firebase プロジェクトを使用する場合は、そのプロジェクトをコンソールから選択します。
    2. プロジェクトの概要ページで [ウェブアプリに Firebase を追加] をクリックします。プロジェクトにアプリがある場合は、プロジェクトの概要ページで [アプリを追加] を選択します。
    3. プロジェクトのカスタマイズ済みのコード スニペットの Initialize Firebase セクションを使用して、frontend/main.js ファイルの次のセクションを入力します。

      // Obtain the following from the "Add Firebase to your web app" dialogue
      // Initialize Firebase
      var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        projectId: "<PROJECT_ID>",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<MESSAGING_SENDER_ID>"
      };
  4. backend/app.yaml ファイルを編集し、Firebase プロジェクト ID を環境変数に入力します。

    runtime: python27
    api_version: 1
    threadsafe: true
    service: backend
    
    handlers:
    - url: /.*
      script: main.app
    
    env_variables:
      # Replace with your Firebase project ID.
      FIREBASE_PROJECT_ID: '<PROJECT_ID>'
      GAE_USE_SOCKETS_HTTPLIB : 'true'
    
  5. frontend/main.js ファイルで、ユーザーに提供するプロバイダを選択して FirebaseUI ログイン ウィジェットを構成します。

      // Firebase log-in widget
      function configureFirebaseLoginWidget() {
        var uiConfig = {
          'signInSuccessUrl': '/',
          'signInOptions': [
            // Leave the lines as is for the providers you want to offer your users.
            firebase.auth.GoogleAuthProvider.PROVIDER_ID,
            firebase.auth.FacebookAuthProvider.PROVIDER_ID,
            firebase.auth.TwitterAuthProvider.PROVIDER_ID,
            firebase.auth.GithubAuthProvider.PROVIDER_ID,
            firebase.auth.EmailAuthProvider.PROVIDER_ID
          ],
          // Terms of service url
          'tosUrl': '<your-tos-url>',
        };
    
        var ui = new firebaseui.auth.AuthUI(firebase.auth());
        ui.start('#firebaseui-auth-container', uiConfig);
      }
    
      // Fetch notes from the backend.
      function fetchNotes() {
        $.ajax(backendHostUrl + '/notes', {
          /* Set header for the XMLHttpRequest to get data from the web server
          associated with userIdToken */
          headers: {
            'Authorization': 'Bearer ' + userIdToken
          }
        }).then(function(data){
          $('#notes-container').empty();
          // Iterate over user data to display user's notes from database.
          data.forEach(function(note){
            $('#notes-container').append($('<p>').text(note.message));
          });
        });
      }
    
      // Sign out a user
      var signOutBtn =$('#sign-out');
      signOutBtn.click(function(event) {
        event.preventDefault();
    
        firebase.auth().signOut().then(function() {
          console.log("Sign out successful");
        }, function(error) {
          console.log(error);
        });
      });
    
      // Save a note to the backend
      var saveNoteBtn = $('#add-note');
      saveNoteBtn.click(function(event) {
        event.preventDefault();
    
        var noteField = $('#note-content');
        var note = noteField.val();
        noteField.val("");
    
        /* Send note data to backend, storing in database with existing data
        associated with userIdToken */
        $.ajax(backendHostUrl + '/notes', {
          headers: {
            'Authorization': 'Bearer ' + userIdToken
          },
          method: 'POST',
          data: JSON.stringify({'message': note}),
          contentType : 'application/json'
        }).then(function(){
          // Refresh notebook display.
          fetchNotes();
        });
    
      });
    
      configureFirebaseLogin();
      configureFirebaseLoginWidget();
    
    });
    
  6. [認証] > [ログイン方法] をクリックして、選択したプロバイダが Firebase コンソール内に保持されるようにします。その後、[ログイン プロバイダ] で、カーソルをプロバイダの上に移動し、鉛筆アイコンをクリックします。

    プロバイダにログイン

    1. [有効] ボタンを切り替え、サードパーティ ID プロバイダの場合は、プロバイダのデベロッパー サイトからプロバイダ ID とシークレットを入力します。Firebase ドキュメントの、FacebookTwitter、および GitHub ガイドの「始める前に」に、具体的な指示が記載されています。プロバイダを有効にしたら、[保存] をクリックします。

      有効ボタンの切り替え

    2. Firebase コンソールの [承認済みドメイン] で [ドメインを追加] をクリックし、App Engine のアプリのドメインを次の形式で入力します。

      [PROJECT_ID].appspot.com
      

      ドメイン名の前に http:// を付けないでください。

依存関係のインストール

  1. backend ディレクトリに移動して、アプリケーション セットアップを完了します。

    cd backend/
    
  2. 依存関係をプロジェクトの lib ディレクトリにインストールします。

    pip install -t lib -r requirements.txt
    
  3. appengine_config.py で、vendor.add() メソッドがライブラリを lib ディレクトリに登録します。

アプリケーションをローカルで実行する

アプリケーションをローカルで実行するには、App Engine ローカル開発用サーバーを使用します。

  1. main.js で次の URL を backendHostURL として追加します。

    http://localhost:8081

  2. アプリケーションのルート ディレクトリに移動します。その後で、開発用サーバーを始動します。

    dev_appserver.py frontend/app.yaml backend/app.yaml
    
  3. ウェブブラウザで http://localhost:8080/ にアクセスします。

サーバーでユーザーを認証する

これで、プロジェクトのセットアップと開発用のアプリケーションの初期化が完了しました。コードを辿りながら、サーバー上で Firebase ID トークンを取得して確認する方法を見てみましょう。

Firebase から ID トークンを取得する

サーバー側の認証の最初の手順は、アクセス トークンの取得と確認です。認証リクエストは、Firebase からの onAuthStateChanged() リスナーを使用して処理されます。

firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    $('#logged-out').hide();
    var name = user.displayName;

    /* If the provider gives a display name, use the name for the
    personal welcome message. Otherwise, use the user's email. */
    var welcomeName = name ? name : user.email;

    user.getIdToken().then(function(idToken) {
      userIdToken = idToken;

      /* Now that the user is authenicated, fetch the notes. */
      fetchNotes();

      $('#user').text(welcomeName);
      $('#logged-in').show();

    });

  } else {
    $('#logged-in').hide();
    $('#logged-out').show();

  }
});

ユーザーがログインすると、コールバックの Firebase getToken() メソッドが JSON Web Token(JWT)形式で Firebase ID トークンを返します。

サーバーでトークンを確認する

ユーザーがログインすると、フロントエンド サービスが AJAX GET リクエストを通してユーザーのノートブック内の既存のメモをフェッチします。これには、ユーザーのデータにアクセスする許可が必要なため、JWT が Bearer スキーマを使用してリクエストの Authorization ヘッダーで送信されます。

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  })

クライアントがサーバー データにアクセスするには、トークンが Firebase によって署名されていることを、サーバーで確認する必要があります。このトークンは、Python 用の Google 認証ライブラリを使用して確認できます。 認証ライブラリの verify_firebase_token 関数を使用して、署名なしトークンを確認し、要求を抽出します。

id_token = request.headers['Authorization'].split(' ').pop()
claims = google.oauth2.id_token.verify_firebase_token(
    id_token, HTTP_REQUEST)
if not claims:
    return 'Unauthorized', 401

各 ID プロバイダは別々の要求セットを送信しますが、それぞれに一意のユーザー ID を持つ 1 つ以上の sub 要求と nameemail などの特定のプロフィール情報を提供する 1 つの要求が含まれています。これを使用して、アプリのユーザー エクスペリエンスをパーソナライズすることができます。

Datastore でのユーザーデータの管理

ユーザーを認証したら、それに関するユーザーのデータを保存してログイン セッションが終わるまで維持する必要があります。以降のセクションでは、メモを Datastore エンティティとして保存し、それらをユーザー ID で区別する方法について説明します。

ユーザー データを保存するエンティティの作成

Datastore では、整数や文字列などの特定のプロパティを使用し、NDB モデルクラスを宣言することによってエンティティを作成できます。Datastore は、種類を基準にしてエンティティにインデックスを付けます。Firenotes の場合、各エンティティの種類は Note です。クエリを出すために、各 Note はキー名付きで保存されます。キー名は、前のセクションの sub 要求から取得されたユーザー ID です。

次のコードは、エンティティのプロパティを設定する方法を示し、エンティティの作成時にモデルクラスとしてコンストラクタ メソッドを使用する方法と、作成後に個別のプロパティを割り当てる方法の両方が示されています。

data = request.get_json()

# Populates note properties according to the model,
# with the user ID as the key name.
note = Note(
    parent=ndb.Key(Note, claims['sub']),
    message=data['message'])

# Some providers do not provide one of these so either can be used.
note.friendly_id = claims.get('name', claims.get('email', 'Unknown'))

新しく作成した Note を Datastore に書き込むには、note オブジェクトに対して put() メソッドを呼び出します。

ユーザー データの取得

特定のユーザー ID に関連付けられたユーザー データを取得するには、NDB の query() メソッドを使用してデータベースを検索し、同じエンティティ グループ内のメモを見つけます。同じグループ内のエンティティ、つまり、祖先パスは、共通のキー名(この場合はユーザー ID)を共有します。

def query_database(user_id):
    """Fetches all notes associated with user_id.

    Notes are ordered them by date created, with most recent note added
    first.
    """
    ancestor_key = ndb.Key(Note, user_id)
    query = Note.query(ancestor=ancestor_key).order(-Note.created)
    notes = query.fetch()

    note_messages = []

    for note in notes:
        note_messages.append({
            'friendly_id': note.friendly_id,
            'message': note.message,
            'created': note.created
        })

    return note_messages

クエリ データをフェッチして、メモをクライアントに表示することができます。

// Fetch notes from the backend.
function fetchNotes() {
  $.ajax(backendHostUrl + '/notes', {
    /* Set header for the XMLHttpRequest to get data from the web server
    associated with userIdToken */
    headers: {
      'Authorization': 'Bearer ' + userIdToken
    }
  }).then(function(data){
    $('#notes-container').empty();
    // Iterate over user data to display user's notes from database.
    data.forEach(function(note){
      $('#notes-container').append($('<p>').text(note.message));
    });
  });
}

アプリのデプロイ

これで Firebase Authentication と App Engine アプリケーションが正常に統合されました。本番環境で動作しているアプリケーションを確認するには、次の手順を実行します。

  1. main.js のバックエンド ホスト URL を https://backend-dot-[PROJECT_ID].appspot.com に変更します。[PROJECT_ID] は実際のプロジェクト ID に置き換えます。
  2. Cloud SDK コマンドライン インターフェースを使用してアプリケーションをデプロイします。

    gcloud app deploy backend/index.yaml frontend/app.yaml backend/app.yaml
    
  3. https://[PROJECT_ID].appspot.com で実動しているアプリケーションを表示します。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするために、以下の手順で App Engine プロジェクトを削除します。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] ページに移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

次のステップ