验证来自 Google Chat 的请求

对于基于 HTTP 端点构建的 Google Chat 应用,本部分将介绍如何验证发送到端点的请求是否来自 Chat。

为了将互动事件分派到 Chat 应用的端点,Google 会向您的服务发出 HTTPS 请求。为了验证请求是否来自 Google,Chat 会在每个 HTTPS 请求的 Authorization 标头中(以及请求正文的 authorizationEventObject.systemIdToken 字段中)包含一个 Google 签名的 OpenID Connect (OIDC) ID 令牌作为不记名令牌。例如:

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

上例中的字符串 AbCdEf123456 是不记名授权令牌。此加密令牌由 Chat 应用的唯一项目特定服务账号 (service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com) 签名,并且在配置 Chat 应用时,audience 字段会设置为为 Chat 应用配置的 HTTP 端点网址。

您可以在 Google Cloud 控制台的 Chat API 配置标签页的连接设置部分复制 Chat 应用的服务账号电子邮件地址:

  1. 在 Google Cloud 控制台中,依次前往菜单 > API 和服务 > 已启用的 API 和服务 > Google Chat API > 配置:

    前往 Google Chat API 配置

  2. 在互动功能 > 连接设置下,复制服务账号电子邮件地址 (service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com)。

如果您是使用 Cloud Run functions 或 Cloud Run 实现 Chat 应用的,那么当您向 Chat 应用的服务账号授予 Cloud Run Invoker (roles/run.invoker) 角色时,Cloud IAM 会自动处理令牌验证。如果您的应用实现了自己的 HTTP 服务器,则可以使用开源 Google API 客户端库验证不记名令牌:

如果令牌未通过 Chat 应用的验证,您的服务应使用 HTTPS 响应代码 401 (Unauthorized) 来响应请求。

使用 Cloud Run functions 对请求进行身份验证

如果您的函数逻辑是使用 Cloud Run functions 或 Cloud Run 实现的,请确保在 Chat 应用连接设置的触发器下配置的 HTTP 端点网址与 Cloud Run 函数端点的网址相对应。

然后,按照以下步骤,将 Chat 应用的服务账号(从 Chat API 配置标签页的连接设置部分复制的 service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com)授权为调用方:

控制台

将函数或服务部署到 Google Cloud 后:

  1. 在 Google Cloud 控制台中,转到 Cloud Run 页面。

    转到 Cloud Run

  2. 在 Cloud Run 服务列表中,点击接收函数旁边的复选框。(请勿点击函数本身。)

  3. 点击屏幕顶部的权限。此时权限面板会打开。

  4. 点击添加主账号。

  5. 在新的主账号字段中,输入 Chat 应用的服务账号电子邮件地址 (service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.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:service-PROJECT_NUMBER@gcp-sa-gsuiteaddons.iam.gserviceaccount.com'

替换以下内容:

  • RECEIVING_FUNCTION:Chat 应用的功能名称。
  • PROJECT_NUMBER:Chat 应用的服务账号电子邮件地址中的项目编号。

使用 ID 令牌对 HTTP 请求进行身份验证

对于 HTTP 端点,请求中的不记名授权令牌是 Google 签名的 OpenID Connect (OIDC) ID 令牌。email 字段设置为 Chat 应用的服务账号电子邮件地址 (service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com),audience 字段设置为配置为接收请求的 HTTP 端点网址。例如,如果聊天应用的配置端点为 https://example.com/app/,则 ID 令牌中的 audience 字段为 https://example.com/app/。

如果您的 HTTP 端点未托管在支持基于 IAM 的身份验证的服务(例如 Cloud Run)上,建议采用此身份验证方法。

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

Java

java/chat/secured-app/src/main/java/com/google/chat/app/secured/App.java
/**
 * Determine whether a Google Workspace add-on request is legitimate.
 * 
 * @param event Event sent from Google Workspace add-on
 * @param authorization Authorization header from the request
 * @return {boolean} Whether the request is legitimate
 */
private boolean verifyAddOnRequest(JsonNode event, String authorization) throws Exception {
  JsonFactory factory = JacksonFactory.getDefaultInstance();

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

  String bearer = authorization.substring("Bearer ".length(), authorization.length());
  GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
  return idToken != null
    && verifier.verify(idToken)
    && idToken.getPayload().getEmailVerified()
    && idToken.getPayload().getEmail().equals(SERVICE_ACCOUNT_EMAIL);
}

Python

python/chat/secured-app/main.py
def verifyAddOnRequest() -> bool:
  """Determine whether a Google Workspace add-on request is legitimate.

  Args:
    request: Request sent from Google Workspace add-on

  Returns:
    Whether the request is legitimate
  """
  try:
    bearer = request.headers.get('Authorization')[len("Bearer "):]
    token = id_token.verify_oauth2_token(bearer, requests.Request(), HTTP_ENDPOINT)
    return token['email'] == SERVICE_ACCOUNT_EMAIL

  except:
    return False

Node.js

node/chat/secured-app/index.js
/**
 * Determine whether a Google Workspace add-on request is legitimate.
 * 
 * @param {Object} req Request sent from Google Workspace add-on
 * @return {boolean} Whether the request is legitimate
 */
async function verifyAddOnRequest(req) {
  try {
    const authorization = req.headers.authorization;
    const idToken = authorization.substring('Bearer '.length, authorization.length);
    const ticket = await new OAuth2Client().verifyIdToken({idToken, audience: HTTP_ENDPOINT});
    return ticket.getPayload().email_verified
        && ticket.getPayload().email === SERVICE_ACCOUNT_EMAIL;
  } catch (unused) {
    return false;
  }
}

非插件的 Chat 扩展应用:验证来自 Google Chat 的请求

以下文档适用于非 Google Workspace 插件的 Chat 应用。如需迁移非插件的 Chat 应用,请参阅将 Google Chat 应用转换为 Google Workspace 插件。

对于未在连接设置下配置 HTTP 端点网址的 Chat 应用,不记名令牌的类型和 audience 字段的值取决于您在配置 Chat 应用时选择的身份验证受众群体类型,并且请求由共享服务账号 chat@system.gserviceaccount.com 签名。

使用 Cloud Run functions 对请求进行身份验证(非插件的聊天应用)

如果您的函数逻辑是使用 Cloud Run functions 实现的,您必须在 Chat 应用连接设置的身份验证受众群体字段中选择 HTTP 端点网址,并确保配置中的 HTTP 端点网址与 Cloud Run 函数端点的网址相对应。

然后,您需要按照以下步骤,将 Google Chat 服务账号 chat@system.gserviceaccount.com 授权为调用方:

控制台

将函数或服务部署到 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 替换为聊天应用的功能名称。

使用 ID 令牌对 HTTP 请求进行身份验证(非插件的 Chat 应用)

如果非插件 Chat 应用的连接设置的身份验证受众群体字段设置为 HTTP 端点网址,则请求中的不记名授权令牌是 Google 签名的 OpenID Connect (OIDC) ID 令牌。email 字段设置为 chat@system.gserviceaccount.com。身份验证受众群体字段设置为您配置的网址,Google Chat 会向您的非插件 Chat 应用发送请求。例如,如果聊天应用的配置端点为 https://example.com/app/,则 ID 令牌中的身份验证受众群体字段为 https://example.com/app/。

以下示例展示了如何使用 Google OAuth 客户端库验证不属于插件的 Google Chat 应用的不记名令牌是否由 Google Chat 签发并以该应用为目标。

Java

java/basic-app/src/main/java/com/google/chat/app/basic/App.java
String CHAT_ISSUER = "chat@system.gserviceaccount.com";
JsonFactory factory = JacksonFactory.getDefaultInstance();

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

GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
return idToken != null
    && verifier.verify(idToken)
    && idToken.getPayload().getEmailVerified()
    && idToken.getPayload().getEmail().equals(CHAT_ISSUER);

Python

python/basic-app/main.py
# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

try:
    # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    request = requests.Request()
    token = id_token.verify_oauth2_token(bearer, request, AUDIENCE)
    return token['email'] == CHAT_ISSUER

except:
    return False

Node.js

node/basic-app/index.js
// Bearer Tokens received by apps will always specify this issuer.
const chatIssuer = 'chat@system.gserviceaccount.com';

// Verify valid token, signed by chatIssuer, intended for a third party.
try {
  const ticket = await client.verifyIdToken({
    idToken: bearer,
    audience: audience
  });
  return ticket.getPayload().email_verified
      && ticket.getPayload().email === chatIssuer;
} catch (unused) {
  return false;
}

使用项目编号 JWT 对请求进行身份验证(非插件的 Chat 应用)

如果非插件 Chat 应用的连接设置的身份验证受众群体字段设置为 Project Number,则请求中的不记名授权令牌是由 chat@system.gserviceaccount.com 签发和签名的自签名 JSON Web 令牌 (JWT)。audience 字段设置为您用于构建非加购项的 Chat 应用的 Google Cloud 项目编号。例如,如果您的 Chat 应用的云项目编号为 1234567890,则 JWT 中的 audience 字段为 1234567890。

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

Java

java/basic-app/src/main/java/com/google/chat/app/basic/App.java
String CHAT_ISSUER = "chat@system.gserviceaccount.com";
JsonFactory factory = JacksonFactory.getDefaultInstance();

GooglePublicKeysManager keyManagerBuilder =
    new GooglePublicKeysManager.Builder(new ApacheHttpTransport(), factory)
        .setPublicCertsEncodedUrl(
            "https://www.googleapis.com/service_accounts/v1/metadata/x509/" + CHAT_ISSUER)
        .build();

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

GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
return idToken != null
    && verifier.verify(idToken)
    && idToken.verifyAudience(Collections.singletonList(AUDIENCE))
    && idToken.verifyIssuer(CHAT_ISSUER);

Python

python/basic-app/main.py
# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

try:
    # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    request = requests.Request()
    certs_url = 'https://www.googleapis.com/service_accounts/v1/metadata/x509/' + CHAT_ISSUER
    token = id_token.verify_token(bearer, request, AUDIENCE, certs_url)
    return token['iss'] == CHAT_ISSUER

except:
    return False

Node.js

node/basic-app/index.js
// Bearer Tokens received by apps will always specify this issuer.
const chatIssuer = 'chat@system.gserviceaccount.com';

// Verify valid token, signed by CHAT_ISSUER, intended for a third party.
try {
  const response = await fetch('https://www.googleapis.com/service_accounts/v1/metadata/x509/' + chatIssuer);
  const certs = await response.json();
  await client.verifySignedJwtWithCertsAsync(
    bearer, certs, audience, [chatIssuer]);
  return true;
} catch (unused) {
  return false;
}