Ruby で Identity-Aware Proxy によるユーザー認証を行う

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

目標

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

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

費用

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

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

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

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

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

  3. Google Cloud プロジェクトで課金が有効になっていることを確認します

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

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

  5. Google Cloud プロジェクトで課金が有効になっていることを確認します

  6. 開発環境を準備します

背景

このチュートリアルでは、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.env["HTTP_X_GOOG_AUTHENTICATED_USER_ID"]

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

ソースコードを作成する

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

    require "base64"
    require "json"
    require "jwt"
    require "net/http"
    require "openssl"
    require "sinatra"
    require "uri"
    
    def certificates
      uri = URI.parse "https://www.gstatic.com/iap/verify/public_key"
      response = Net::HTTP.get_response uri
      JSON.parse response.body
    end
    
    set :certificates, certificates
    
    def get_metadata item_name
      endpoint = "http://metadata.google.internal"
      path = "/computeMetadata/v1/project/#{item_name}"
      uri = URI.parse endpoint + path
    
      http = Net::HTTP.new uri.host, uri.port
      request = Net::HTTP::Get.new uri.request_uri
      request["Metadata-Flavor"] = "Google"
      response = http.request request
      response.body
    end
    
    def audience
      project_number = get_metadata "numeric-project-id"
      project_id = get_metadata "project-id"
      "/projects/#{project_number}/apps/#{project_id}"
    end
    
    set :audience, audience
    
    def validate_assertion assertion
      a_header = Base64.decode64 assertion.split(".")[0]
      key_id = JSON.parse(a_header)["kid"]
      cert = OpenSSL::PKey::EC.new settings.certificates[key_id]
      info = JWT.decode assertion, cert, true, algorithm: "ES256", aud: settings.audience
      [info[0]["email"], info[0]["sub"]]
    rescue StandardError => e
      puts "Failed to validate assertion: #{e}"
      [nil, nil]
    end
    
    get "/" do
      assertion = request.env["HTTP_X_GOOG_IAP_JWT_ASSERTION"]
      email, _user_id = validate_assertion assertion
      "<h1>Hello #{email}</h1>"
    end

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

  2. ターミナル ウィンドウで、新しく作成した app.rb と同じフォルダに移動し、bundle init を実行します。

  3. 結果の Gemfile に次を追加します。

    gem "jwt", "~> 2.2"
    gem "sinatra", "~> 2.0"

    Gemfile には App Engine で読み込む必要のある、非標準の Ruby ライブラリが列挙されています。

    • Sinatra は、このアプリで使用する ruby のウェブ フレームワークです。

    • jwt は、JWT のチェックとデコードの機能を提供します。

  4. Gemfile に列挙されている依存関係をインストールします。

    bundle install
    
  5. app.yaml という名前のファイルを作成し、次のテキストを貼り付けます。

    runtime: ruby25
    entrypoint: bundle exec ruby app.rb

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

コードについて

このセクションでは、app.rb ファイル内のコードが機能する仕組みを説明します。アプリを実行するだけの場合は、アプリのデプロイ セクションまでスキップできます。

  • アプリがホームページへの HTTP GET リクエストを受信すると、Sinatra は次のブロックを実行します。

    get "/" do
      assertion = request.env["HTTP_X_GOOG_IAP_JWT_ASSERTION"]
      email, _user_id = validate_assertion assertion
      "<h1>Hello #{email}</h1>"
    end
  • このブロックは、IAP が受信リクエストから追加した JWT アサーション ヘッダー値を取得し、その暗号署名付き値を検証するメソッドを呼び出します。返される最初の値(メール)は、Sinatra が作成する最小ウェブページで使用されます。

    def validate_assertion assertion
      a_header = Base64.decode64 assertion.split(".")[0]
      key_id = JSON.parse(a_header)["kid"]
      cert = OpenSSL::PKey::EC.new settings.certificates[key_id]
      info = JWT.decode assertion, cert, true, algorithm: "ES256", aud: settings.audience
      [info[0]["email"], info[0]["sub"]]
    rescue StandardError => e
      puts "Failed to validate assertion: #{e}"
      [nil, nil]
    end
  • validate_assertion メソッドは、サードパーティ jwt ライブラリからの JWT.decode メソッドを使用して、アサーションが適切に署名されていることを確認し、アサーションからペイロード情報を抽出します。この情報は、認証済みユーザーのメールアドレスと永続的な一意の ID です。アサーションをデコードできない場合は、このメソッドはそれらの値ごとに nil を返し、エラーを記録するためのメッセージを出力します。

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

    def audience
      project_number = get_metadata "numeric-project-id"
      project_id = get_metadata "project-id"
      "/projects/#{project_number}/apps/#{project_id}"
    end
    
    set :audience, audience
  • Google Cloud プロジェクトの数値 ID と名前を検索してソースコードに挿入できますが、audience 関数を使用しても、すべての App Engine アプリで利用できる標準メタデータ サービスをクエリすることにより、同じことが可能です。メタデータ サービスはアプリコードの外部にあるため、その結果はSinatraフレームワークのsettingsオブジェクトに保存されます。

    def get_metadata item_name
      endpoint = "http://metadata.google.internal"
      path = "/computeMetadata/v1/project/#{item_name}"
      uri = URI.parse endpoint + path
    
      http = Net::HTTP.new uri.host, uri.port
      request = Net::HTTP::Get.new uri.request_uri
      request["Metadata-Flavor"] = "Google"
      response = http.request request
      response.body
    end
  • App Engine のメタデータ サービス(および他の Google Cloud コンピューティング サービスの類似のメタデータ サービス)は、ウェブサイトのように見え、標準のウェブクエリによって照会されます。ただし、実際には、外部サイトではなく、実行中のアプリに関する要求された情報を返す内部機能であるため、https リクエストの代わりに http リクエストを使用しても安全です。ここで App Engine メタデータ サービスは、JWT アサーションの対象オーディエンスを定義するために必要な現在の Google Cloud 識別子を取得するために使用されます。

    def certificates
      uri = URI.parse "https://www.gstatic.com/iap/verify/public_key"
      response = Net::HTTP.get_response uri
      JSON.parse response.body
    end
    
    set :certificates, certificates
  • デジタル署名を検証するには、署名者の公開鍵証明書が必要です。Google では、現在使用されているすべての公開鍵証明書を返すウェブサイトを提供しています。これらの結果は、同じアプリ インスタンス内で再び必要になる場合に備えてキャッシュに保存されます。

アプリのデプロイ

これで、アプリをデプロイし、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 コンソールで、[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. Google Cloud コンソールで、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

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

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

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

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

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

  1. Google Cloud コンソールで、[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 を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。