Google Chat からのリクエストを確認する

このセクションでは、HTTP エンドポイント上に構築された Google Chat アプリについて、エンドポイントへのリクエストの送信元が Chat であることを確認する方法について説明します。

インタラクション イベントを Chat アプリのエンドポイントにディスパッチするために、Google はサービスにリクエストを行います。リクエストが Google から送信されていることを確認するため、Chat は、エンドポイントへのすべての HTTPS リクエストの Authorization ヘッダーに署名なしトークンを含めます。次に例を示します。

POST
Host: yourappurl.com
Authorization: Bearer AbCdEf123456
Content-Type: application/json
User-Agent: Google-Dynamite

上記の例の文字列 AbCdEf123456 は、署名なし認証トークンです。これは Google が生成する暗号トークンです。署名なしトークンのタイプと audience フィールドの値は、Chat アプリの構成時に選択した認証オーディエンスのタイプによって異なります。

Cloud Functions または Cloud Run を使用して Chat 用アプリを実装している場合、Cloud IAM はトークンの検証を自動的に処理します。Google Chat サービス アカウントを承認済み起動元として追加するだけで済みます。アプリが独自の HTTP サーバーを実装している場合は、オープンソースの Google API クライアント ライブラリを使用して署名なしトークンを確認できます。

Chat アプリのトークンが検証されない場合、サービスは HTTPS レスポンス コード 401 (Unauthorized) でリクエストに応答する必要があります。

Cloud Functions または Cloud Run を使用してリクエストを認証する

関数のロジックが Cloud Functions または Cloud Run を使用して実装されている場合は、Chat アプリの接続設定の [Authentication Audience] フィールドで App URL を選択し、構成のアプリ URL が Cloud Functions または Cloud Run エンドポイントの URL に対応していることを確認する必要があります。

次に、Google Chat サービス アカウント chat@system.gserviceaccount.com を起動元として承認する必要があります。

次の手順は、Cloud Functions(第 1 世代)を使用する方法を示しています。

コンソール

関数を Google Cloud にデプロイした後:

  1. Google Cloud コンソールで、[Cloud Functions] ページに移動します。

    Cloud Functions に移動

  2. [Cloud Functions] リストで、受信側関数の横にあるチェックボックスをオンにします。(関数自体はクリックしないでください)。

  3. 画面の上部の [権限] をクリックします。[権限] パネルが開きます。

  4. [プリンシパルを追加] をクリックします。

  5. [新しいプリンシパル] フィールドに「chat@system.gserviceaccount.com」と入力します。

  6. [ロールを選択] プルダウン メニューから [Cloud Functions] > [Cloud Functions 起動元] ロールを選択します。

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

gcloud

gcloud functions add-iam-policy-binding コマンドを使用します。

gcloud functions add-iam-policy-binding RECEIVING_FUNCTION \
  --member='serviceAccount:chat@system.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

RECEIVING_FUNCTION は、Chat アプリの関数の名前に置き換えます。

次の手順は、Cloud Functions(第 2 世代)または Cloud Run サービスを使用する方法を示しています。

コンソール

関数またはサービスを Google Cloud にデプロイした後:

  1. Google Cloud コンソールで、[Cloud Run] ページに移動します。

    Cloud Run に移動

  2. Cloud Run サービスリストで、受信側関数の横にあるチェックボックスをオンにします。(関数自体はクリックしないでください)。

  3. 画面の上部の [権限] をクリックします。[権限] パネルが開きます。

  4. [プリンシパルを追加] をクリックします。

  5. [新しいプリンシパル] フィールドに「chat@system.gserviceaccount.com」と入力します。

  6. [ロールを選択] プルダウン メニューから [Cloud Run] > [Cloud Run 起動元] ロールを選択します。

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

gcloud

gcloud functions add-invoker-policy-binding コマンドを使用します。

gcloud functions add-invoker-policy-binding RECEIVING_FUNCTION \
  --member='serviceAccount:chat@system.gserviceaccount.com'

RECEIVING_FUNCTION は、Chat アプリの関数の名前に置き換えます。

アプリ URL ID トークンを使用してリクエストを認証する

Chat アプリの接続設定の [Authentication Audience] フィールドが App URL に設定されている場合、リクエストの署名なし認証トークンは Google によって署名された OpenID Connect(OIDC)ID トークンです。email フィールドが chat@system.gserviceaccount.com に設定されています。audience フィールドは、Chat アプリにリクエストを送信するように Google Chat を構成した URL に設定されます。たとえば、Chat アプリの構成済みエンドポイントが https://example.com/app/ の場合、ID トークンの audience フィールドは https://example.com/app/ になります。

次のサンプルは、署名なしトークンが Google Chat によって発行され、Google OAuth クライアント ライブラリを使用してアプリをターゲットにしていることを確認する方法を示しています。

Java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collections;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.JsonFactory;

/** Tool for verifying JWT Tokens for Apps in Google Chat. */
public class JWTVerify {
  // Bearer Tokens received by apps will always specify this issuer.
  static String CHAT_ISSUER = "chat@system.gserviceaccount.com";

  // Intended audience of the token, which is the URL of the app.
  static String AUDIENCE = "https://example.com/app/";

  // Get this value from the request's Authorization HTTPS header.
  // For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456".
  static String BEARER_TOKEN = "AbCdEf123456";

  public static void main(String[] args) throws GeneralSecurityException, IOException {
    JsonFactory factory = new GsonFactory();

    GoogleIdTokenVerifier verifier =
        new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), factory)
        .setAudience(Collections.singletonList(AUDIENCE))
        .build();

    GoogleIdToken idToken = GoogleIdToken.parse(factory, BEARER_TOKEN);
    if (idToken == null) {
      System.out.println("Token cannot be parsed");
      System.exit(-1);
    }

    // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    if (!verifier.verify(idToken)
        || !idToken.getPayload().getEmailVerified()
        || !idToken.getPayload().getEmail().equals(CHAT_ISSUER)) {
      System.out.println("Invalid token");
      System.exit(-1);
    }

    // Token originates from Google and is targeted to a specific client.
    System.out.println("The token is valid");
  }
}

Python

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

# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

# Intended audience of the token, which is the URL of the app.
AUDIENCE = 'https://example.com/app/'

# Get this value from the request's Authorization HTTPS header.
# For example, for 'Authorization: Bearer AbCdEf123456' use 'AbCdEf123456'.
BEARER_TOKEN = 'AbCdEf123456'

try:
  # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  request = requests.Request()
  token = id_token.verify_oauth2_token(BEARER_TOKEN, request, AUDIENCE)

  if token['email'] != CHAT_ISSUER:
    sys.exit('Invalid token')
except:
  sys.exit('Invalid token')

# Token originates from Google and is targeted to a specific client.
print('The token is valid')

Node.js

import {OAuth2Client} from 'google-auth-library';

// Bearer Tokens received by apps will always specify this issuer.
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';

// Intended audience of the token, which is the URL of the app.
const AUDIENCE = 'https://example.com/app/';

// Get this value from the request's Authorization HTTPS header.
// For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456"
const BEARER_TOKEN = 'AbCdEf123456';

const client = new OAuth2Client();

async function verify() {
  // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  try {
    const ticket = await client.verifyIdToken({
      idToken: BEARER_TOKEN,
      audience: AUDIENCE
    });
    if (!ticket.getPayload().email_verified
        || ticket.getPayload().email !== CHAT_ISSUER) {
      throw new Error('Invalid issuer');
    }
  } catch (unused) {
    console.error('Invalid token');
    process.exit(1);
  }

  // Token originates from Google and is targeted to a specific client.
  console.log('The token is valid');
}

verify();

プロジェクト番号 JWT を使用してリクエストを認証する

Chat アプリの接続設定の Authentication Audience フィールドが Project Number に設定されている(または未設定)場合、リクエスト内の署名なし認証トークンは自己署名の JSON ウェブトークン(JWT)で、chat@system.gserviceaccount.com によって発行され、署名されます。audience フィールドは、Chat アプリのビルドに使用した Google Cloud プロジェクト番号に設定されます。たとえば、Chat アプリの Cloud プロジェクト番号が 1234567890 の場合、JWT の audience フィールドは 1234567890 になります。

次のサンプルは、署名なしトークンが Google Chat によって発行され、Google OAuth クライアント ライブラリを使用してプロジェクトをターゲットにしていることを確認する方法を示しています。

Java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collections;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.JsonFactory;

/** Tool for verifying JWT Tokens for Apps in Google Chat. */
public class JWTVerify {
  // Bearer Tokens received by apps will always specify this issuer.
  static String CHAT_ISSUER = "chat@system.gserviceaccount.com";

  // Url to obtain the public certificate for the issuer.
  static String PUBLIC_CERT_URL_PREFIX =
      "https://www.googleapis.com/service_accounts/v1/metadata/x509/";

  // Intended audience of the token, which is the project number of the app.
  static String AUDIENCE = "1234567890";

  // Get this value from the request's Authorization HTTPS header.
  // For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456".
  static String BEARER_TOKEN = "AbCdEf123456";

  public static void main(String[] args) throws GeneralSecurityException, IOException {
    JsonFactory factory = new GsonFactory();

    GooglePublicKeysManager.Builder keyManagerBuilder =
        new GooglePublicKeysManager.Builder(new ApacheHttpTransport(), factory);

    String certUrl = PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER;
    keyManagerBuilder.setPublicCertsEncodedUrl(certUrl);

    GoogleIdTokenVerifier.Builder verifierBuilder =
        new GoogleIdTokenVerifier.Builder(keyManagerBuilder.build());
    verifierBuilder.setIssuer(CHAT_ISSUER);
    GoogleIdTokenVerifier verifier = verifierBuilder.build();

    GoogleIdToken idToken = GoogleIdToken.parse(factory, BEARER_TOKEN);
    if (idToken == null) {
      System.out.println("Token cannot be parsed");
      System.exit(-1);
    }

    // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    if (!verifier.verify(idToken)
        || !idToken.verifyAudience(Collections.singletonList(AUDIENCE))
        || !idToken.verifyIssuer(CHAT_ISSUER)) {
      System.out.println("Invalid token");
      System.exit(-1);
    }

    // Token originates from Google and is targeted to a specific client.
    System.out.println("The token is valid");
  }
}

Python

import sys

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

# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

# Url to obtain the public certificate for the issuer.
PUBLIC_CERT_URL_PREFIX = 'https://www.googleapis.com/service_accounts/v1/metadata/x509/'

# Intended audience of the token, which will be the project number of the app.
AUDIENCE = '1234567890'

# Get this value from the request's Authorization HTTPS header.
# For example, for 'Authorization: Bearer AbCdEf123456' use 'AbCdEf123456'.
BEARER_TOKEN = 'AbCdEf123456'

try:
  # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  request = requests.Request()
  certs_url = PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER
  token = id_token.verify_token(BEARER_TOKEN, request, AUDIENCE, certs_url)

  if token['iss'] != CHAT_ISSUER:
    sys.exit('Invalid issuer')
except:
  sys.exit('Invalid token')

# Token originates from Google and is targeted to a specific client.
print('The token is valid')

Node.js

import fetch from 'node-fetch';
import {OAuth2Client} from 'google-auth-library';

// Bearer Tokens received by apps will always specify this issuer.
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';

// Url to obtain the public certificate for the issuer.
const PUBLIC_CERT_URL_PREFIX =
    'https://www.googleapis.com/service_accounts/v1/metadata/x509/';

// Intended audience of the token, which is the project number of the app.
const AUDIENCE = '1234567890';

// Get this value from the request's Authorization HTTPS header.
// For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456"
const BEARER_TOKEN = 'AbCdEf123456';

const client = new OAuth2Client();

/** Verifies JWT Tokens for Apps in Google Chat. */
async function verify() {
  // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  try {
    const response = await fetch(PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER);
    const certs = await response.json();
    const ticket = await client.verifySignedJwtWithCertsAsync(
        BEARER_TOKEN, certs, AUDIENCE, [CHAT_ISSUER]);
  } catch (unused) {
    console.error('Invalid token');
    process.exit(1);
  }

  // Token originates from Google and is targeted to a specific client.
  console.log('The token is valid');
}

verify();