Python 用 Cloud Identity-Aware Proxy によるユーザー認証

App Engine などの Google Cloud マネージド プラットフォーム上で実行するアプリは、Identity-Aware Proxy(IAP)を使用することにより、ユーザー認証やセッション管理の手間を省いてアプリへのアクセスを制御できます。IAP は、アプリへのアクセスを制御できるだけでなく、新しい HTTP ヘッダーの形式でメールアドレスや永続的な識別子などの認証済みユーザーに関する情報をアプリに提供することもできます。

目標

  • IAP を使用することにより、App Engine アプリのユーザーに自分自身を認証するように要求する。

  • アプリでユーザーの ID にアクセスして、現在のユーザーの認証済みメールアドレスを表示する。

料金

このドキュメントでは、Google Cloud の次の課金対象のコンポーネントを使用します。

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

このドキュメントに記載されているタスクの完了後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. Google Cloud アカウントにログインします。Google Cloud を初めて使用する場合は、アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

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

  3. Google Cloud CLI をインストールします。
  4. gcloud CLI を初期化するには:

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

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

  6. Google Cloud CLI をインストールします。
  7. gcloud CLI を初期化するには:

    gcloud init

背景

このチュートリアルでは、IAP を使用してユーザーを認証します。これは、いくつかのアプローチのうちの 1 つにすぎません。ユーザーのさまざまな認証方法の詳細については、認証のコンセプトセクションをご覧ください。

Hello user-email-address アプリ

このチュートリアル用のアプリは、最小の Hello world App Engine アプリで、「Hello world」の代わりに「Hello user-email-address」を表示する特別な機能を備えています。ここで、user-email-address は認証済みユーザーのメールアドレスです。

この機能は、IAP がアプリに渡す各ウェブ リクエストに追加する認証情報を調べることで可能になります。アプリに届くリクエストには 3 つの新しいリクエスト ヘッダーが追加されます。最初の 2 つのヘッダーは、ユーザーの識別に使用できる書式なしテキスト文字列です。3 つ目のヘッダーは、同じ情報を含む暗号署名付きオブジェクトです。

  • X-Goog-Authenticated-User-Email: ユーザーのメールアドレスでユーザーが識別されます。アプリで回避できる場合は、個人情報を保存しないようにしてください。このアプリはデータを保存しません。ユーザーにエコーバックするだけです。

  • X-Goog-Authenticated-User-Id: Google が割り当てたこのユーザー ID は、ユーザーに関する情報は表示しませんが、ログインしているユーザーが過去に表示されたユーザーと同じであることをアプリが認識できるようにします。

  • X-Goog-Iap-Jwt-Assertion: インターネット ウェブ リクエスト以外に、IAP を迂回してきた他のクラウドアプリからのウェブ リクエストを受け入れるように Google Cloud アプリを構成できます。アプリがこのように構成されている場合は、このようなリクエストのヘッダーが偽造されている可能性があります。上記のいずれかの書式なしテキスト ヘッダーを使用する代わりに、この暗号署名付きヘッダーを使用して、情報が Google から提供されたものであることを確認できます。ユーザーのメールアドレスと永続的なユーザー ID の両方をこの署名付きヘッダーの一部として使用できます。

アプリが、インターネット ウェブ リクエストのみを受け入れ、アプリに対する IAP サービスを無効にできないように構成されている場合は、一意のユーザー ID を取得するコードは 1 行で済みます。

user_id = request.headers.get('X-Goog-Authenticated-User-ID')

ただし、回復性の高いアプリは予期せぬ構成や環境問題などの障害を想定しておく必要があるため、代わりに、暗号署名付きヘッダーを使用して確認する関数を作成することをおすすめします。ヘッダーの署名は偽造できないため、検証時に、識別を返すのに使用できます。

ソースコードを作成する

  1. テキスト エディタを使用して、main.py という名前のファイルを作成し、次のコードを貼り付けます。

    import sys
    
    from flask import Flask
    app = Flask(__name__)
    
    CERTS = None
    AUDIENCE = None
    
    def certs():
        """Returns a dictionary of current Google public key certificates for
        validating Google-signed JWTs. Since these change rarely, the result
        is cached on first request for faster subsequent responses.
        """
        import requests
    
        global CERTS
        if CERTS is None:
            response = requests.get(
                'https://www.gstatic.com/iap/verify/public_key'
            )
            CERTS = response.json()
        return CERTS
    
    def get_metadata(item_name):
        """Returns a string with the project metadata value for the item_name.
        See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
        possible item_name values.
        """
        import requests
    
        endpoint = 'http://metadata.google.internal'
        path = '/computeMetadata/v1/project/'
        path += item_name
        response = requests.get(
            '{}{}'.format(endpoint, path),
            headers={'Metadata-Flavor': 'Google'}
        )
        metadata = response.text
        return metadata
    
    def audience():
        """Returns the audience value (the JWT 'aud' property) for the current
        running instance. Since this involves a metadata lookup, the result is
        cached when first requested for faster future responses.
        """
        global AUDIENCE
        if AUDIENCE is None:
            project_number = get_metadata('numeric-project-id')
            project_id = get_metadata('project-id')
            AUDIENCE = '/projects/{}/apps/{}'.format(
                project_number, project_id
            )
        return AUDIENCE
    
    def validate_assertion(assertion):
        """Checks that the JWT assertion is valid (properly signed, for the
        correct audience) and if so, returns strings for the requesting user's
        email and a persistent user ID. If not valid, returns None for each field.
        """
        from jose import jwt
    
        try:
            info = jwt.decode(
                assertion,
                certs(),
                algorithms=['ES256'],
                audience=audience()
                )
            return info['email'], info['sub']
        except Exception as e:
            print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
            return None, None
    
    @app.route('/', methods=['GET'])
    def say_hello():
        from flask import request
    
        assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
        email, id = validate_assertion(assertion)
        page = "<h1>Hello {}</h1>".format(email)
        return page

    この main.py ファイルの詳細については、このチュートリアルの後半のコードについてセクションで説明します。

  2. requirements.txt という名前の別のファイルを作成し、次のコードを貼り付けます。

    Flask==2.1.2
    cryptography==37.0.2
    python-jose[cryptography]==3.3.0
    requests==2.27.1

    requirements.txt ファイルには、App Engine で読み込むために必要な標準以外の Python ライブラリがリストされています。

    • Flask は、アプリで使用される Python ウェブ フレームワークです。

    • cryptography は、強力な暗号機能を提供するモジュールです。

    • python-jose[cryptography] は、JWT のチェックおよびデコード機能を提供します。

    • requests は、ウェブサイトからデータを取得します。

  3. app.yaml という名前のファイルを作成し、次のテキストを入力します。

    runtime: python37

    app.yaml ファイルは、コードが必要とする言語環境を App Engine に指示します。

コードの説明

このセクションでは、main.py 内のコードの機能について説明します。アプリを実行するだけの場合は、アプリのデプロイ セクションまでスキップできます。

次のコードが main.py ファイルに格納されています。ホームページへの HTTP GET リクエストがアプリで受信されると、Flask フレームワークは say_hello 関数を呼び出します。

@app.route('/', methods=['GET'])
def say_hello():
    from flask import request

    assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
    email, id = validate_assertion(assertion)
    page = "<h1>Hello {}</h1>".format(email)
    return page

say_hello 関数は、IAP が受信リクエストから追加した JWT アサーション ヘッダー値を取得し、その暗号署名付き値を検証する関数を呼び出します。返される最初の値(メール)は、作成されて返される最小ウェブページで使用されます。

def validate_assertion(assertion):
    """Checks that the JWT assertion is valid (properly signed, for the
    correct audience) and if so, returns strings for the requesting user's
    email and a persistent user ID. If not valid, returns None for each field.
    """
    from jose import jwt

    try:
        info = jwt.decode(
            assertion,
            certs(),
            algorithms=['ES256'],
            audience=audience()
            )
        return info['email'], info['sub']
    except Exception as e:
        print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
        return None, None

validate_assertion 関数は、サードパーティ jose ライブラリからの jwt.decode 関数を使用して、アサーションが適切に署名されていることを確認し、アサーションからペイロード情報を抽出します。この情報は、認証済みユーザーのメールアドレスと永続的な一意の ID です。アサーションをデコードできない場合は、この関数はそれらの値ごとに None を返し、エラーを記録するためのメッセージを出力します。

JWT アサーションを検証するには、アサーションに署名した機関(この場合は Google)の公開鍵証明書と、アサーションの対象オーディエンスを把握している必要があります。App Engine アプリの場合、オーディエンスは Google Cloud のプロジェクト識別情報を含む文字列です。この関数は、それらの証明書とその前の関数からのオーディエンス文字列を取得します。

def audience():
    """Returns the audience value (the JWT 'aud' property) for the current
    running instance. Since this involves a metadata lookup, the result is
    cached when first requested for faster future responses.
    """
    global AUDIENCE
    if AUDIENCE is None:
        project_number = get_metadata('numeric-project-id')
        project_id = get_metadata('project-id')
        AUDIENCE = '/projects/{}/apps/{}'.format(
            project_number, project_id
        )
    return AUDIENCE

Google Cloud プロジェクトの数値 ID と名前を検索してソースコードに挿入できますが、audience 関数を使用しても、すべての App Engine アプリで利用できる標準メタデータ サービスをクエリすることにより、同じことが可能です。メタデータ サービスはアプリコードの外部にあるため、その結果はグローバル変数に保存され、以降の呼び出しでメタデータを検索する必要はありません。

def get_metadata(item_name):
    """Returns a string with the project metadata value for the item_name.
    See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
    possible item_name values.
    """
    import requests

    endpoint = 'http://metadata.google.internal'
    path = '/computeMetadata/v1/project/'
    path += item_name
    response = requests.get(
        '{}{}'.format(endpoint, path),
        headers={'Metadata-Flavor': 'Google'}
    )
    metadata = response.text
    return metadata

App Engine のメタデータ サービス(および他の Google Cloud コンピューティング サービスの類似のメタデータ サービス)は、ウェブサイトのように見え、標準のウェブクエリによって照会されます。ただし、実際には、外部サイトではなく、実行中のアプリに関する要求された情報を返す内部機能であるため、https リクエストの代わりに http リクエストを使用しても安全です。これは、JWT アサーションの対象オーディエンスを定義するために必要な現在の Google Cloud 識別子を取得するために使用されます。

def certs():
    """Returns a dictionary of current Google public key certificates for
    validating Google-signed JWTs. Since these change rarely, the result
    is cached on first request for faster subsequent responses.
    """
    import requests

    global CERTS
    if CERTS is None:
        response = requests.get(
            'https://www.gstatic.com/iap/verify/public_key'
        )
        CERTS = response.json()
    return CERTS

デジタル署名を検証するには、署名者の公開鍵証明書が必要です。Google では、現在使用されているすべての公開鍵証明書を返すウェブサイトを提供しています。これらの結果は、同じアプリ インスタンス内で再び必要になる場合に備えてキャッシュに保存されます。

アプリのデプロイ

これで、アプリをデプロイして、次いで Cloud IAP でユーザーが認証しないとユーザーがアプリにアクセスできないようにできます。

  1. ターミナル ウィンドウで、app.yaml ファイルが格納されたディレクトリに移動して、アプリを App Engine にデプロイします。

    gcloud app deploy
    
  2. プロンプトが表示されたら、最寄のリージョンを選択します。

  3. デプロイ操作を続行するかどうかを尋ねられたら、Y と入力します。

    数分以内に、アプリがインターネット上に公開されます。

  4. 次のようにしてアプリを表示します。

    gcloud app browse
    

    出力で、web-site-url(アプリのウェブアドレス)をコピーします。

  5. ブラウザ ウィンドウで、web-site-url を貼り付けてアプリを開きます。

    IAP をまだ使用していないのでメールが表示されないため、ユーザー情報がアプリに送信されません。

IAP を有効にする

これで App Engine インスタンスが存在するようになり、IAP を使用して保護できます。

  1. Google Cloud Console で、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

  2. 今回はこのプロジェクト用の認証オプションを初めて有効にしたため、IAP を使用するには OAuth 同意画面を構成する必要があることを示すメッセージが表示されます。

    [同意画面を構成] をクリックします。

  3. [認証情報] ページの [OAuth 同意画面] タブで、次のフィールドに値を入力します。

    • アカウントが Google Workspace の組織にある場合は、[外部] を選択して [作成] をクリックします。開始すると、アプリは明示的に許可されたユーザーのみが利用できます。

    • [アプリケーション名] フィールドに、IAP Example と入力します。

    • [サポートメール] フィールドに、メールアドレスを入力します。

    • [承認済みドメイン] フィールドに、アプリの URL のホスト名部分(iap-example-999999.uc.r.appspot.com など)を入力します。フィールドにホスト名を入力したら、Enter キーを押します。

    • [アプリケーション ホームページ リンク] フィールドに、アプリの URL(https://iap-example-999999.uc.r.appspot.com/ など)を入力します。

    • [Application privacy policy line] フィールドでは、テスト用にホームページ リンクと同じ URL を使用します。

  4. [保存] をクリックします。認証情報の作成が要求されたら、ウィンドウを閉じます。

  5. Cloud Console で、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

  6. ページを更新するには、[Refresh](更新)をクリックします。ページに、保護可能なリソースのリストが表示されます。

  7. [IAP] 列でクリックしてアプリの IAP をオンにします。

  8. ブラウザで、再度 web-site-url にアクセスします。

  9. ウェブページの代わりに、自分を認証するためのログイン画面が表示されます。ログインしようとすると、アプリにアクセス可能なユーザーのリストが IAP にないため、アクセスが拒否されます。

アプリに承認済みユーザーを追加する

  1. Cloud Console で、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

  2. App Engine アプリのチェックボックスをオンにしてから、[プリンシパルを追加] をクリックします。

  3. allAuthenticatedUsers と入力してから、[Cloud IAP/IAP で保護されたウェブアプリ ユーザー] 役割を選択します。

  4. [保存] をクリックします。

これで、Google で認証可能なすべてのユーザーがアプリにアクセスできます。必要に応じて、1 人または複数のユーザーやグループをプリンシパルとして追加するだけで、さらにアクセスを制限できます。

  • Gmail または Google Workspace のメールアドレス

  • Google グループ メールアドレス

  • Google Workspaceのドメイン名

アプリにアクセスする

  1. ブラウザで、web-site-url にアクセスします。

  2. ページを更新するには、[更新] 更新をクリックします。

  3. ログイン画面で、Google 認証情報を使用してログインします。

    ページに、メールアドレスが記載された「Hello user-email-address」ページが表示されます。

    以前と同じページが表示される場合は、IAP を有効にしたブラウザで新しいリクエストが完全に更新されない問題が発生している可能性があります。すべてのブラウザ ウィンドウを閉じてから、もう一度開いて、やり直してください。

認証のコンセプト

アプリが、そのユーザーを認証し、承認されたユーザーのみにアクセスを制限可能な方法がいくつかあります。次のセクションで、一般的な認証方法をアプリの負荷が高い順に一覧表示します。

オプション メリット デメリット
アプリ認証
  • インターネット接続の有無にかかわらず、どのプラットフォームでもアプリを実行できる
  • 他のサービスを使用して認証を管理する必要がない
  • アプリはユーザー認証情報を安全に管理して、漏洩しないよう保護する必要がある
  • アプリはログインしているユーザーのセッション データを維持する必要がある
  • アプリはユーザー登録、パスワード変更、パスワード復元を提供する必要がある
OAuth2
  • デベロッパー ワークステーションなど、インターネットに接続されたあらゆるプラットフォームでアプリを実行できる
  • アプリはユーザー登録、パスワード変更、パスワード復元の各機能を必要としない
  • ユーザー情報漏洩のリスクが他のサービスに委任される
  • 新しいログイン セキュリティ対策がアプリの外部で処理される
  • ユーザーが ID サービスに登録する必要がある
  • アプリはログインしているユーザーのセッション データを維持する必要がある
IAP
  • アプリはユーザー、認証、セッション状態を管理するためのコードを必要としない
  • アプリは侵害される可能性のあるユーザー認証情報を持たない
  • アプリはサービスでサポートされているプラットフォームでしか実行できない。具体的には、App Engine などの IAP をサポートする特定の Google Cloud サービスです。

アプリ管理認証

この方法では、アプリがユーザー認証のすべての側面を独自に管理します。アプリは、ユーザー認証情報の独自のデータベースを維持してユーザー セッションを管理し、ユーザー アカウントとパスワードの管理、ユーザー認証情報のチェック、認証されたログインごとのユーザー セッションの発行、チェック、更新を行う必要があります。次の図は、アプリ管理認証方法を示しています。

アプリケーション管理フロー

図に示すように、ユーザーがログインすると、アプリがそのユーザーのセッションに関する情報を作成して維持します。ユーザーがアプリにリクエストを発行する場合は、そのリクエストにアプリが検証するセッション情報が含まれている必要があります。

このアプローチの主なメリットは、自己完結型であることと、アプリの制御下で行われることです。アプリをインターネット上で使用可能にする必要さえありません。主なデメリットは、アプリがすべてのアカウント管理機能を提供し、すべての機密性の高い認証情報データを保護する必要があることです。

OAuth2 を使用した外部認証

アプリ内ですべてを処理する代わりに、Google などの外部の ID サービスを使用して、すべてのアカウント情報と機能を処理し、機密性の高い認証情報を保護することをおすすめします。ユーザーがアプリにログインしようとすると、リクエストが ID サービスにリダイレクトされ、そこでユーザーが認証されてから、必要な認証情報と一緒にリクエストがアプリに返されます。詳細については、エンドユーザーとして認証をご覧ください。

次の図は、OAuth2 方式を使用した外部認証を示しています。

OAuth2 のフロー

図のフローは、ユーザーがアプリにアクセスするためのリクエストを送信するところから始まります。直接応答する代わりに、アプリは、ユーザーのブラウザを Google の Identity Platform にリダイレクトします。そこで、Google にログインするためのページが表示されます。ログインに成功すると、ユーザーのブラウザがアプリに戻されます。このリクエストには、認証されたユーザーに関してアプリが検索可能な情報が含まれており、アプリはユーザーに応答できます。

この方法には、アプリにとっての多くのメリットがあります。アプリがすべてのアカウント管理機能とリスクを外部サービスに委任することで、アプリを変更せずにログインとアカウント セキュリティを向上させることができます。ただし、上の図に示すように、この方法を使用するには、アプリからインターネットにアクセスできる必要があります。また、アプリには、ユーザーの認証後にセッションを管理する責任があります。

Identity-Aware Proxy

このチュートリアルで扱う 3 つ目のアプローチは、IAP を使用して、認証とアプリに対するなんらかの変更を伴うセッション管理をすべて処理する方法です。IAP は、アプリへのすべてのウェブ リクエストを傍受して、認証されていないリクエストをブロックし、それ以外のリクエストはリクエストごとにユーザー ID データを追加してアプリに渡します。

リクエストの処理を次の図に示します。

IAP のフロー

ユーザーからの要求は、IAP によって傍受され、認証されていない要求がブロックされます。認証されたユーザーが許可されたユーザーのリストに含まれている場合は、認証されたリクエストがアプリに渡されます。IAP を介して渡されたリクエストには、要求を行ったユーザーを識別するヘッダーが追加されます。

アプリは、ユーザー アカウントやセッション情報を処理する必要がなくなります。ユーザーの一意の識別子を知る必要があるオペレーションでは、受信ウェブ リクエストから直接取得できます。ただし、App Engine やロードバランサなど、IAP をサポートするコンピューティング サービスでのみ使用できます。ローカル開発マシンで IAP を使用することはできません。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。

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

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