アプリ ID の OIDC ID トークンへの移行

Python 2 ランタイムで実行されているアプリで別の App Engine アプリにリクエストを送信すると、App Engine App Identity API を使用してその ID を表明できます。リクエストを受信したアプリでは、この ID を使用して、リクエストを処理するかどうかを決定できます。

Python 3 アプリで別の App Engine アプリにリクエストを送信する際に ID を表明する必要がある場合は、OpenID Connect(OIDC)ID トークンを使用できます。これは、Google の OAuth 2.0 API によって発行およびデコードされます。

OIDC ID トークンを使用して ID を表明、確認する方法の概要は次のとおりです。

  1. 「アプリ A」という名前の App Engine アプリでは、Google Cloud ランタイム環境から ID トークンを取得します。
  2. アプリ A では、別の App Engine アプリであるアプリ B にリクエストを送信する直前に、このトークンをリクエスト ヘッダーに追加します。
  3. アプリ B は Google の OAuth 2.0 API を使用して、トークン ペイロードを確認します。デコードされたペイロードには、アプリ A の確認済み ID が、アプリ A のデフォルト サービス アカウントのメールアドレスの形式で含まれます。
  4. アプリ B では、ペイロード内の ID と、応答可能な ID のリストを比較します。許可されたアプリから送信されたリクエストの場合、アプリ B でリクエストを処理して応答します。

OAuth 2.0 プロセス

このガイドでは、OpenID Connect(OIDC)ID トークンを使用して ID を表明するように App Engine アプリを更新する方法と、リクエストを処理する前に ID トークンを使用して ID を検証するように他の App Engine アプリを更新する方法について説明します。

App Identity API と OIDC API の主な違い

  • Python 2 ランタイム内のアプリでは、明示的に ID を表明する必要はありません。アプリが httpliburlliburllib2 Python ライブラリ、または App Engine URL 取得サービスを使用して送信リクエストを送信する場合、ランタイムは、App Engine URL 取得サービスを使用してリクエストを実行します。リクエストが appspot.com ドメインに送信されると、URL 取得サービスは X-Appengine-Inbound-Appid ヘッダーをリクエストに追加して、リクエスト元のアプリの ID を自動的に表明します。このヘッダーには、アプリのアプリケーション ID(プロジェクト ID)が含まれています。

    Python 3 ランタイムのアプリでは、Google Cloud ランタイム環境から OIDC ID トークンを取得し、リクエスト ヘッダーに追加することによって ID を明示的に表明する必要があります。リクエストに OIDC ID トークンが含まれるように、他の App Engine アプリにリクエストを送信するすべてのコードを更新する必要があります。

  • リクエスト内の X-Appengine-Inbound-Appid ヘッダーには、リクエストを送信したアプリのプロジェクト ID が含まれています。

    Google の OIDC ID トークンのペイロードは、アプリ自体のプロジェクト ID を直接識別しません。トークンは、サービス アカウントのメールアドレスを提供して、アプリが実行されているサービス アカウントを識別します。トークン ペイロードからユーザー名を抽出するコードを追加する必要があります。

    このサービス アカウントがプロジェクトのアプリレベルのデフォルトの App Engine サービス アカウントである場合、プロジェクト ID はサービス アカウントのメールアドレスで確認できます。アドレスのユーザー名の部分はプロジェクト ID と同じです。この場合、受信側のアプリコードは、リクエストを許可するプロジェクト ID のリストでこの値を検索します。

    ただし、リクエスト元のアプリがデフォルトの App Engine サービス アカウントではなく、ユーザー管理のサービス アカウントを使用している場合、受信側のアプリはそのサービス アカウントの ID のみを確認します。リクエスト元のアプリのプロジェクト ID が定義されている必要はありません。その場合は、受信側のアプリで、許可されたプロジェクト ID のリストではなく、許可されたサービス アカウントのメールアドレスのリストを維持する必要があります。

  • URL Fetch API 呼び出しの割り当ては、トークン付与に対する Google の OAuth 2.0 API の割り当てとは異なります。1 日に付与できるトークンの最大数は、Google Cloud コンソールの OAuth 同意画面で確認できます。URL 取得、App Identity API、Google の OAuth 2.0 API ではいずれも料金は発生しません。

移行プロセスの概要

OIDC API を使用して ID の表明と検証を行うように Python アプリを移行するには:

  1. 他の App Engine アプリにリクエストを送信するときに ID を表明する必要があるアプリの場合:

    1. アプリが Python 3 環境で動作し、ID トークンに移行するまで待ちます。

      Python 2 ランタイムで ID トークンを使用することは可能ですが、Python 2 での手順は複雑で、Python 3 ランタイムで実行するようにアプリを更新するまで、一時的にのみ必要とされます。

    2. Python 3 でアプリが動作するようになったら、ID トークンをリクエストし、そのトークンをリクエスト ヘッダーに追加するようにアプリを更新します。

  2. リクエストを処理する前に ID の確認が必要なアプリの場合:

    1. まず、Python 2 アプリをアップグレードして、ID トークンと App Identity API の ID の両方をサポートします。これにより、アプリで App Identity API を使用する Python 2 アプリまたは ID トークンを使用する Python 3 アプリからのリクエストの確認と処理が可能になります。

    2. アップグレードした Python 2 アプリが安定したら、Python 3 ランタイムに移行します。アプリがレガシーアプリからのリクエストをサポートする必要がないことが確実になるまで、ID トークンと App Identity API の ID の両方のサポートを続行してください。

    3. 以前の App Engine アプリからのリクエストを処理する必要がなくなったら、App Identity API の ID を確認するコードを削除します。

  3. アプリをテストした後、最初にリクエストを処理するアプリをデプロイします。次に ID トークンを使用して更新された Python 3 アプリをデプロイし、ID を表明します。

ID の表明

Python 3 環境でアプリが動作するようになるまで待機し、次の手順を行って ID トークンで ID を表明するようにアプリをアップグレードします。

  1. google-auth クライアント ライブラリをインストールします。

  2. Google の OAuth 2.0 API から ID トークンをリクエストし、リクエストを送信する前にそのトークンをリクエスト ヘッダーに追加するコードを追加します。

  3. 更新をテストします。

Python 3 アプリ用 google-auth クライアント ライブラリのインストール

Python 3 アプリで google-auth クライアント ライブラリを使用できるようにするには、app.yaml ファイルと同じフォルダに requirements.txt ファイルを作成し、次の行を追加します。

     google-auth

アプリのデプロイ時に、App Engine で requirements.txt ファイル内に定義されているすべての依存関係がダウンロードされます。

ローカル開発では、venv などの仮想環境に依存関係をインストールすることをおすすめします。

ID を表明するためのコードの追加

コード全体を検索し、他の App Engine アプリにリクエストを送信しているすべてのインスタンスを見つけます。これらのインスタンスを更新して、リクエストを送信する前に次の操作を行います。

  1. 次のインポートを追加します。

    from google.auth.transport import requests as reqs
    from google.oauth2 import id_token
    
  2. google.oauth2.id_token.fetch_id_token(request, audience) を使用して ID トークンを取得します。メソッド呼び出しに次のパラメータを含めます。

    • request: 送信しようとしているリクエスト オブジェクトを渡します。
    • audience: リクエストの送信先アプリの URL を渡します。これにより、トークンがリクエストにバインドされ、そのトークンが別のアプリで使用できなくなります。

      具体的には、アプリにカスタム ドメインを使用している場合でも、リクエストを受信する特定のサービスに対して App Engine で作成された appspot.com URL を渡すことをおすすめします。

  3. リクエスト オブジェクトで、次のヘッダーを設定します。

    'Authorization': 'ID {}'.format(token)
    

例:

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from flask import Flask, render_template, request
from google.auth.transport import requests as reqs
from google.oauth2 import id_token
import requests

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/", methods=["POST"])
def make_request():
    url = request.form["url"]
    token = id_token.fetch_id_token(reqs.Request(), url)

    resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})

    message = f"Response when calling {url}:\n\n"
    message += resp.text

    return message, 200, {"Content-type": "text/plain"}

ID の表明のための更新のテスト

アプリをローカルで実行し、アプリで ID トークンを正常に送信できるかどうかをテストするには:

  1. ローカル環境でデフォルトの App Engine サービス アカウントの認証情報を使用できるようにするには、次の手順を行います(Google OAuth API で ID トークンを生成するためにこれらの認証情報が必要です)。

    1. 次の gcloud コマンドを入力して、プロジェクトのデフォルト App Engine アカウントのサービス アカウント キーを取得します。

      gcloud iam service-accounts keys create ~/key.json --iam-account project-ID@appspot.gserviceaccount.com

      project-ID は、Google Cloud プロジェクトの ID に置き換えます。

      これで、マシンにサービス アカウント キーファイルがダウンロードされました。このファイルは任意の場所に移動できます。名前の変更も可能です。このファイルはサービス アカウントとしての認証に使用できるため、安全な場所に保管してください。ファイルを紛失した場合や、権限のないユーザーにファイルが公開されている場合は、サービス アカウント キーを削除して新しく作成します。

    2. 次のコマンドを入力します。

      <code>export GOOGLE_APPLICATION_CREDENTIALS=<var>service-account-key</var></code>
      

    service-account-key は、ダウンロードしたサービス アカウント キーが含まれるファイルの絶対パス名に置き換えます。

  2. GOOGLE_APPLICATION_CREDENTIALS 環境変数をエクスポートしたのと同じシェルで、Python アプリを起動します。

  3. アプリからリクエストを送信して、リクエストが成功することを確認します。リクエストを受信し、ID トークンを使用して ID を確認できるアプリがまだない場合は、次の操作を行います。

    1. 「受信」サンプルアプリをダウンロードします。
    2. サンプルの main.py ファイルで、Google Cloud プロジェクトの ID を allowed_app_ids に追加します。例:

       allowed_app_ids = [
          '<APP_ID_1>',
          '<APP_ID_2>',
          'my-project-id'
        ]
      
    3. Python 2 のローカルの開発用サーバーで、更新したサンプルを実行します。

リクエストの確認と処理

リクエストの処理前に、ID トークンまたは App Identity API の ID を使用するように Python 2 アプリをアップグレードするには:

  1. google-auth クライアント ライブラリをインストールします。

  2. 以下を行うようにコードを更新します。

    1. リクエストに X-Appengine-Inbound-Appid ヘッダーが含まれている場合は、そのヘッダーを使用して ID を確認します。Python 2 などの従来のランタイムで実行されるアプリには、このヘッダーが含まれます。

    2. リクエストに X-Appengine-Inbound-Appid ヘッダーが含まれていない場合は、OIDC ID トークンの有無を確認します。このトークンが存在する場合は、トークン ペイロードを確認し、送信者の ID を確認します

  3. 更新をテストします。

Python 2 アプリ用 google-auth クライアント ライブラリのインストール

google-auth クライアント ライブラリを Python 2 アプリで使用できるようにするには:

  1. app.yaml ファイルと同じフォルダに requirements.txt ファイルを作成し、次の行を追加します。

     google-auth==1.19.2
    

    Python 2.7 アプリがサポートされているため、Cloud Logging クライアント ライブラリの 1.19.2 バージョンの使用をおすすめします。

  2. まだ指定されていない場合は、アプリの app.yaml ファイルで libraries セクションで SSL ライブラリを指定します。

    libraries:
    - name: ssl
      version: latest
    
  3. サードパーティ ライブラリの保存先ディレクトリ(例: lib/)を作成します。次に、pip install を使用して、ディレクトリにライブラリをインストールします。例:

    pip install -t lib -r requirements.txt
    
  4. app.yaml ファイルと同じフォルダに appengine_config.py ファイルを作成します。次のコードを appengine_config.py ファイルに追加します。

    # appengine_config.py
    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set path to your libraries folder.
    path = 'lib'
    # Add libraries installed in the path folder.
    vendor.add(path)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(path)
    

    上記の例の appengine_config.py ファイルは、lib フォルダが現在の作業ディレクトリにあることを前提としています。lib が常に現在の作業ディレクトリにあることを保証できない場合は、lib フォルダのフルパスを指定します。例:

    import os
    path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib')
    

ローカル開発では、Python 2 の virtualenv などの仮想環境に依存関係をインストールすることをおすすめします。

リクエストの確認用のコードの更新

コードを検索し、X-Appengine-Inbound-Appid ヘッダーの値を取得しているすべてのインスタンスを見つけます。次のことを行って、これらのインスタンスを更新します。

  1. 次のインポートを追加します。

    from google.auth.transport import requests as reqs
    from google.oauth2 import id_token
    
  2. 受信リクエストに X-Appengine-Inbound-Appid ヘッダーが含まれていない場合は、Authorization ヘッダーを見つけて、その値を取得します。

    ヘッダーの値は「ID: token」の形式になります。

  3. デコードされたトークン ペイロードを確認して取得するには、google.oauth2.id_token.verify_oauth2_token(token, request, audience) を使用します。メソッド呼び出しに次のパラメータを含めます。

    • token: 受信リクエストから抽出したトークンを渡します。
    • request: 新しい google.auth.transport.Request オブジェクトを渡します。

    • audience: 現在のアプリ(確認リクエストを送信しているアプリ)の URL を渡します。Google の認可サーバーは、この URL と、トークンの生成時に指定した URL を比較します。URL が一致しない場合、トークンは確認されず、認可サーバーからエラーが返されます。

  4. verify_oauth2_token メソッドでは、デコードされたトークン ペイロードを返します。これには、いくつかの名前 / 値ペア(トークンを生成したアプリのデフォルトのサービス アカウントのメールアドレスが含まれる)が含まれます。

  5. トークン ペイロードのメールアドレスからユーザー名を抽出します。

    ユーザー名は、リクエストを送信したアプリのプロジェクト ID と同じです。これは、以前に X-Appengine-Inbound-Appid ヘッダーで返された値と同じ値です。

  6. ユーザー名 / プロジェクト ID が、許可されているプロジェクト ID のリストに含まれている場合は、リクエストを処理します。

例:

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Authenticate requests coming from other App Engine instances.
"""

from google.oauth2 import id_token
from google.auth.transport import requests

import logging
import webapp2


def get_app_id(request):
    # Requests from App Engine Standard for Python 2.7 will include a
    # trustworthy X-Appengine-Inbound-Appid. Other requests won't have
    # that header, as the App Engine runtime will strip it out
    incoming_app_id = request.headers.get("X-Appengine-Inbound-Appid", None)
    if incoming_app_id is not None:
        return incoming_app_id

    # Other App Engine apps can get an ID token for the App Engine default
    # service account, which will identify the application ID. They will
    # have to include at token in an Authorization header to be recognized
    # by this method.
    auth_header = request.headers.get("Authorization", None)
    if auth_header is None:
        return None

    # The auth_header must be in the form Authorization: Bearer token.
    bearer, token = auth_header.split()
    if bearer.lower() != "bearer":
        return None

    try:
        info = id_token.verify_oauth2_token(token, requests.Request())
        service_account_email = info["email"]
        incoming_app_id, domain = service_account_email.split("@")
        if domain != "appspot.gserviceaccount.com":  # Not App Engine svc acct
            return None
        else:
            return incoming_app_id
    except Exception as e:
        # report or log if desired, as here:
        logging.warning("Request has bad OAuth2 id token: {}".format(e))
        return None


class MainPage(webapp2.RequestHandler):
    allowed_app_ids = ["other-app-id", "other-app-id-2"]

    def get(self):
        incoming_app_id = get_app_id(self.request)

        if incoming_app_id is None:
            self.abort(403)

        if incoming_app_id not in self.allowed_app_ids:
            self.abort(403)

        self.response.write("This is a protected page.")


app = webapp2.WSGIApplication([("/", MainPage)], debug=True)

ID の検証のための更新のテスト

アプリで ID トークンまたは X-Appengine-Inbound-Appid ヘッダーのいずれかを使用してリクエストを検証できることをテストするには、Python 2 のローカルの開発用サーバーでアプリを実行し、(App Engine API を使用する)Python 2 アプリと、ID トークンを送信する Python 3 アプリからリクエストを送信します。

ID トークンを送信するようにアプリを更新していない場合:

  1. 「リクエスト側の」サンプルアプリをダウンロードします。

  2. アプリを表明するための更新のテストの説明に従って、サービス アカウントの認証情報をローカル環境に追加します。

  3. 標準の Python 3 コマンドを使用して、Python 3 サンプルアプリを起動します。

  4. サンプルアプリからリクエストを送信して、リクエストが成功することを確認します。

アプリのデプロイ

アプリをデプロイする準備ができたら、以下を行います。

  1. App Engine でアプリをテストします。

  2. アプリがエラーなしで実行されている場合、トラフィック分割を使用して、更新したアプリのトラフィックを徐々に増やします。更新したアプリへのトラフィックを増やす前に、問題が発生していないか細かくモニタリングして確認してください。

ID の表明のための別のサービス アカウントの使用

ID トークンをリクエストするとき、そのリクエストではデフォルトで App Engine のデフォルト サービス アカウントの ID を使用します。トークンを検証するとき、トークン ペイロードにデフォルト サービス アカウントのメールアドレスが含まれています。これは、アプリのプロジェクト ID にマッピングされます。

App Engine のデフォルト サービス アカウントには、デフォルトで非常に高い権限があります。Google Cloud プロジェクト全体の表示や編集が可能であるため、ほとんどの場合、アプリで Cloud サービスを使用した認証が必要なときに使用することは適切ではありません。

ただし、リクエストを送信したアプリの ID を ID トークンのみを使用して確認しているため、アプリ ID を表明するときにデフォルトのサービス アカウントを使用することは安全です。このプロセス中は、サービス アカウントに付与された実際の権限は考慮されず、不要です。

ID トークン リクエストに別のサービス アカウントを使用する場合は、次の手順を行います。

  1. GOOGLE_APPLICATION_CREDENTIALS という名前の環境変数に、サービス アカウントの認証情報が含まれる JSON ファイルのパスを設定します。これらの認証情報を安全に格納するための推奨事項をご覧ください。

  2. google.oauth2.id_token.fetch_id_token(request, audience) を使用して ID トークンを取得します。

  3. このトークンを検証すると、新しいサービス アカウントのメールアドレスがトークン ペイロードに含まれています。