验证来自 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,并确保配置中的应用网址与 Cloud Functions 函数或 Cloud Run 端点的网址对应。

然后,您需要授权 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 Invoker 角色。

  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 Invoker 角色。

  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 应用的函数名称。

使用应用网址 ID 令牌对请求进行身份验证

如果 Chat 应用连接设置的 Authentication Audience 字段设置为 App URL,则请求中的不记名授权令牌是由 Google 签名的 OpenID Connect (OIDC) ID 令牌email 字段设置为 chat@system.gserviceaccount.comaudience 字段设置为您配置 Google Chat 以向您的 Chat 应用发送请求的网址。例如,如果您的 Chat 应用的已配置端点为 https://example.com/app/,则 ID 令牌中的 audience 字段为 https://example.com/app/

以下示例展示了如何使用 Google OAuth 客户端库验证不记名令牌是否由 Google Chat 颁发,以及是否以您的应用为目标。

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(或未设置),则请求中的不记名授权令牌是由 chat@system.gserviceaccount.com 颁发和签名的自签名 JSON Web 令牌 (JWT)audience 字段设置为您用于构建 Chat 应用的 Google Cloud 项目编号。例如,如果您的 Chat 应用的 Cloud 项目编号为 1234567890,则 JWT 中的 audience 字段为 1234567890

以下示例展示了如何使用 Google OAuth 客户端库验证不记名令牌是否由 Google Chat 发放,以及是否针对您的项目。

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();