验证来自 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 Run functions 实现的 Chat 应用,Cloud IAM 会自动处理令牌验证。您必须将 Google Chat 服务账号添加为授权调用方。 如果您的应用实现自己的 HTTP 服务器,您可以使用开源 Google API 客户端库验证不记名令牌 :

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

使用 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 替换为 Chat 应用的函数名称。

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

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

如果您的 HTTP 端点未托管在支持基于 IAM 的身份验证的服务(例如 Cloud Run)上,建议使用此身份验证方法。使用此方法时,您的 HTTP 服务需要有关其运行端点网址的信息,但不需要有关云项目编号的信息。

以下示例展示了如何使用 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();

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 应用连接设置的“身份验证受众群体”字段设置为 Project Number,则请求中的不记名授权令牌是由 chat@system.gserviceaccount.com 发布和签名的自签名 JSON Web 令牌 (JWT)audience 字段设置为您用于构建 Chat 应用的 Google Cloud 项目编号。例如,如果 Chat 应用的 Cloud 项目编号为 1234567890,则 JWT 中的 audience 字段为 1234567890

如果您希望使用云项目编号(而不是 HTTP 端点网址)来验证请求,建议使用此身份验证方法。例如,如果您希望随着时间的推移更改端点网址,同时保留相同的 Cloud 项目编号,或者如果您希望对多个 Cloud 项目编号使用相同的端点,并希望将 audience 字段与 Cloud 项目编号列表进行比较。

以下示例展示了如何使用 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;
}