Node.js を使用したユーザーの認証


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. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Install the Google Cloud CLI.
  4. To initialize the gcloud CLI, run the following command:

    gcloud init
  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Install the Google Cloud CLI.
  7. To initialize the gcloud CLI, run the following command:

    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 行で済みます。

userId = req.header('X-Goog-Authenticated-User-ID') :? null;

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

ソースコードを作成する

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

    const express = require('express');
    const metadata = require('gcp-metadata');
    const {OAuth2Client} = require('google-auth-library');
    
    const app = express();
    const oAuth2Client = new OAuth2Client();
    
    // Cache externally fetched information for future invocations
    let aud;
    
    async function audience() {
      if (!aud && (await metadata.isAvailable())) {
        let project_number = await metadata.project('numeric-project-id');
        let project_id = await metadata.project('project-id');
    
        aud = '/projects/' + project_number + '/apps/' + project_id;
      }
    
      return aud;
    }
    
    async function validateAssertion(assertion) {
      if (!assertion) {
        return {};
      }
    
      // Check that the assertion's audience matches ours
      const aud = await audience();
    
      // Fetch the current certificates and verify the signature on the assertion
      const response = await oAuth2Client.getIapPublicKeys();
      const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
        assertion,
        response.pubkeys,
        aud,
        ['https://cloud.google.com/iap']
      );
      const payload = ticket.getPayload();
    
      // Return the two relevant pieces of information
      return {
        email: payload.email,
        sub: payload.sub,
      };
    }
    
    app.get('/', async (req, res) => {
      const assertion = req.header('X-Goog-IAP-JWT-Assertion');
      let email = 'None';
      try {
        const info = await validateAssertion(assertion);
        email = info.email;
      } catch (error) {
        console.log(error);
      }
      res.status(200).send(`Hello ${email}`).end();
    });
    
    // Start the server
    const PORT = process.env.PORT || 8080;
    app.listen(PORT, () => {
      console.log(`App listening on port ${PORT}`);
      console.log('Press Ctrl+C to quit.');
    });
    

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

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

    {
      "name": "iap-authentication",
      "description": "Minimal app to use authentication information from IAP.",
      "private": true,
      "license": "Apache-2.0",
      "author": "Google LLC",
      "repository": {
        "type": "git",
        "url": "https://github.com/GoogleCloudPlatform/getting-started-nodejs.git"
      },
      "engines": {
        "node": ">=12.0.0"
      },
      "scripts": {
        "start": "node app.js",
        "test": "mocha --exit test/*.test.js"
      },
      "dependencies": {
        "express": "^4.17.1",
        "gcp-metadata": "^5.0.0",
        "google-auth-library": "^8.0.0"
      },
      "devDependencies": {
        "mocha": "^9.0.0",
        "supertest": "^6.0.0"
      }
    }
    

    package.json ファイルには、アプリに必要な Node.js 依存関係がすべて一覧表示されます。jsonwebtoken は、JWT のチェックおよびデコード機能を提供します。

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

    # Copyright 2019 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.
    
    runtime: nodejs10
    

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

コードの説明

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

次のコードが app.js ファイルに格納されています。アプリが HTTP GET を受信した場合、/ のスイッチケースが呼び出されます。

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

app.get('/', async (req, res) => {
  const assertion = req.header('X-Goog-IAP-JWT-Assertion');
  let email = 'None';
  try {
    const info = await validateAssertion(assertion);
    email = info.email;
  } catch (error) {
    console.log(error);
  }
  res.status(200).send(`Hello ${email}`).end();
});

validateAssertion 関数は、google-auth-library からの verifySignedJwtWithCertsAsync() 関数を使用して、アサーションが適切に署名されていることを確認し、アサーションからペイロード情報を抽出します。この情報は、認証済みユーザーのメールアドレスと永続的な一意の ID です。アサーションをデコードできない場合、この関数はエラーをログに記録するためのメッセージをスローして出力します。

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

async function validateAssertion(assertion) {
  if (!assertion) {
    return {};
  }

  // Check that the assertion's audience matches ours
  const aud = await audience();

  // Fetch the current certificates and verify the signature on the assertion
  const response = await oAuth2Client.getIapPublicKeys();
  const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
    assertion,
    response.pubkeys,
    aud,
    ['https://cloud.google.com/iap']
  );
  const payload = ticket.getPayload();

  // Return the two relevant pieces of information
  return {
    email: payload.email,
    sub: payload.sub,
  };
}

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

async function audience() {
  if (!aud && (await metadata.isAvailable())) {
    let project_number = await metadata.project('numeric-project-id');
    let project_id = await metadata.project('project-id');

    aud = '/projects/' + project_number + '/apps/' + project_id;
  }

  return aud;
}

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

const response = await oAuth2Client.getIapPublicKeys();

デジタル署名を検証するには、署名者の公開鍵証明書が必要です。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. 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 サービスにリダイレクトされ、そこでユーザーが認証されてから、必要な認証情報と一緒にリクエストがアプリに返されます。詳しくは、Using OAuth 2.0 for Web Server Applications(英語)をご覧ください。

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

OAuth2 のフロー

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

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

Identity-Aware Proxy

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

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

IAP のフロー

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

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

クリーンアップ

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

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.