Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/gchat-endpoint-url-audience.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@chat-adapter/gchat": patch
---

fix(gchat): accept `endpointUrl` as a direct-webhook verifier and verify each token type correctly

When a Google Chat app's connection setting **Authentication audience** is set
to **HTTP endpoint URL** — Google's recommended option for HTTP-hosted apps
not behind Cloud Run IAM, and the only mode available for Workspace Add-on
Chat apps — incoming tokens are Google OIDC ID tokens whose `aud` is the
endpoint URL rather than the GCP project number. Previously the adapter only
verified against `googleChatProjectNumber`, so URL-audience tokens always
failed with 401 Unauthorized. The adapter now accepts `endpointUrl` as a
direct-webhook verifier (including in the constructor's fail-closed check),
validating the OIDC token's audience plus the Google Chat issuer email claims
(`chat@system.gserviceaccount.com`, or the
`service-{projectNumber}@gcp-sa-gsuiteaddons.iam.gserviceaccount.com` service
identity for Workspace Add-on Chat apps) with `email_verified: true` — a
public endpoint URL audience alone is not sufficient to forge a request.

Project-number-audience tokens are now verified per Google's reference
implementation: they are JWTs self-signed by
`chat@system.gserviceaccount.com`, so the adapter checks them against that
service account's X.509 certificates with issuer
`chat@system.gserviceaccount.com` (previously it used `verifyIdToken`, which
only accepts Google OIDC issuers and certs and therefore rejected every real
project-number token). When both verifiers are configured, either token type
is accepted.

The adapter still infers an endpoint URL from incoming requests for
button-click action routing only — that inferred value is never used as a
JWT verification audience, and inference now only happens after a request
has passed verification (or verification was explicitly disabled), because
`request.url` derives from the attacker-controllable `Host` header in
serverless runtimes.
28 changes: 24 additions & 4 deletions apps/docs/content/adapters/official/gchat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ bot.onNewMention(async (thread, message) => {
googleChatProjectNumber: {
type: "string",
description:
"GCP project number for direct webhook JWT verification.",
"GCP project number for direct webhook JWT verification. Use when the Chat app's authentication audience is set to 'Project number'.",
},
endpointUrl: {
type: "string",
description:
"Public URL of the webhook endpoint. Required for routing button click actions to your app, and used as an accepted JWT audience for direct webhooks when the Chat app's authentication audience is set to 'HTTP endpoint URL'.",
},
impersonateUser: {
type: "string",
Expand All @@ -125,7 +130,7 @@ bot.onNewMention(async (thread, message) => {
}}
/>

One of `googleChatProjectNumber`, `pubsubAudience`, or `disableSignatureVerification: true` is required — the constructor throws otherwise. Configure the verifier(s) for each transport you actually receive.
One of `googleChatProjectNumber`, `endpointUrl`, `pubsubAudience`, or `disableSignatureVerification: true` is required — the constructor throws otherwise. Configure the verifier(s) for each transport you actually receive.

## Authentication

Expand Down Expand Up @@ -196,10 +201,25 @@ Required for Workspace Events subscriptions and initiating DMs.

The two transports share one HTTP endpoint, so each verifier only covers its own request shape:

- **Direct webhooks** — Google Chat sends a signed JWT whose `aud` claim is your GCP project number. Configure with `googleChatProjectNumber`.
- **Direct webhooks** — Google Chat sends a signed JWT in the `Authorization: Bearer …` header. The expected `aud` claim depends on how the Chat app is configured (see [Verify requests from Google Chat](https://developers.google.com/workspace/chat/verify-requests-from-chat)).
- **Pub/Sub push** — Cloud Pub/Sub sends a signed OIDC JWT whose audience is whatever you configured on the push subscription. Configure with `pubsubAudience`.

If you only configure `googleChatProjectNumber`, incoming Pub/Sub-shaped requests are rejected with HTTP 401 — and vice versa. Configure both if you receive both.
If you only configure a direct-webhook verifier, incoming Pub/Sub-shaped requests are rejected with HTTP 401 — and vice versa. Configure both transports if you receive both.

#### Which direct-webhook option do I need?

| Your Chat app | Token type & `aud` | Signed by | Set |
| -------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Standalone Chat app, **Authentication audience: Project number** (in Chat API config) | self-signed JWT, `aud` = project number | `chat@system.gserviceaccount.com` (its own X.509 certs) | `googleChatProjectNumber` |
| Standalone Chat app, **Authentication audience: HTTP endpoint URL** | Google OIDC ID token, `aud` = endpoint URL | Google (`email`: `chat@system.gserviceaccount.com`) | `endpointUrl` |
| **Workspace Add-on Chat app** (built via Google Workspace Marketplace SDK; the audience is hardcoded) | Google OIDC ID token, `aud` = endpoint URL | Google (`email`: `service-{projectNumber}@gcp-sa-gsuiteaddons.iam.gserviceaccount.com`) | `endpointUrl` |
| Mixed across envs / not sure | varies | varies | both `googleChatProjectNumber` and `endpointUrl` |

The two token types are verified differently, matching [Google's reference implementation](https://developers.google.com/workspace/chat/verify-requests-from-chat): endpoint-URL tokens are standard OIDC ID tokens checked against Google's public certs plus the Chat service-account `email` claim; project-number tokens are self-signed by `chat@system.gserviceaccount.com` and checked against that service account's own X.509 certificates. When both `googleChatProjectNumber` and `endpointUrl` are set, either token type is accepted. If you don't know which mode your app uses, look at an incoming token: an `email` claim containing `gcp-sa-gsuiteaddons` means it's a Workspace Add-on (URL audience).

<Callout type="info">
Workspace Add-on Chat apps don't expose an "Authentication audience" radio; their token `aud` is always the endpoint URL. Set `endpointUrl` for these.
</Callout>

### Limitations

Expand Down
31 changes: 22 additions & 9 deletions packages/adapter-gchat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,17 @@ Set `GOOGLE_CHAT_IMPERSONATE_USER` to an admin user email in your domain (e.g.,

## Configuration

All options are auto-detected from environment variables when not provided.
Most options are auto-detected from environment variables when not provided.
`endpointUrl` must be passed in config.

| Option | Required | Description |
|--------|----------|-------------|
| `credentials` | No* | Service account credentials JSON. Auto-detected from `GOOGLE_CHAT_CREDENTIALS` |
| `useApplicationDefaultCredentials` | No | Use Application Default Credentials. Auto-detected from `GOOGLE_CHAT_USE_ADC` |
| `pubsubTopic` | No | Pub/Sub topic for Workspace Events. Auto-detected from `GOOGLE_CHAT_PUBSUB_TOPIC` |
| `pubsubAudience` | No† | Expected JWT audience for Pub/Sub webhook verification. Auto-detected from `GOOGLE_CHAT_PUBSUB_AUDIENCE` |
| `googleChatProjectNumber` | No† | GCP project number for direct webhook JWT verification. Auto-detected from `GOOGLE_CHAT_PROJECT_NUMBER` |
| `googleChatProjectNumber` | No† | GCP project number for direct webhook JWT verification when the Chat app's authentication audience is "Project number". Auto-detected from `GOOGLE_CHAT_PROJECT_NUMBER` |
| `endpointUrl` | No† | Public webhook URL for button click routing and direct webhook JWT verification when the Chat app's authentication audience is "HTTP endpoint URL". Must be passed in config |
| `disableSignatureVerification` | No† | Disable JWT verification entirely (development only). Auto-detected from `GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true` |
| `impersonateUser` | No | User email for domain-wide delegation. Auto-detected from `GOOGLE_CHAT_IMPERSONATE_USER` |
| `auth` | No | Custom auth object (advanced) |
Expand All @@ -187,7 +189,7 @@ All options are auto-detected from environment variables when not provided.

*Either `credentials`, `GOOGLE_CHAT_CREDENTIALS` env var, `useApplicationDefaultCredentials`, or `GOOGLE_CHAT_USE_ADC=true` is required.

†One of `googleChatProjectNumber`, `pubsubAudience`, or `disableSignatureVerification: true` is required — the constructor throws otherwise. Configure the verifier(s) for each transport you actually receive; requests of a shape whose verifier is unconfigured are rejected with HTTP 401.
†One of `googleChatProjectNumber`, `endpointUrl`, `pubsubAudience`, or `disableSignatureVerification: true` is required — the constructor throws otherwise. Configure the verifier(s) for each transport you actually receive; requests of a shape whose verifier is unconfigured are rejected with HTTP 401.

## Environment variables

Expand All @@ -198,8 +200,8 @@ GOOGLE_CHAT_CREDENTIALS={"type":"service_account",...}
GOOGLE_CHAT_PUBSUB_TOPIC=projects/your-project/topics/chat-events
GOOGLE_CHAT_IMPERSONATE_USER=admin@yourdomain.com

# Webhook verification — at least one of the three is required
GOOGLE_CHAT_PROJECT_NUMBER=123456789 # For direct webhook JWT verification
# Webhook verification — at least one verifier or the explicit opt-out is required
GOOGLE_CHAT_PROJECT_NUMBER=123456789 # Direct webhooks with project-number audience
GOOGLE_CHAT_PUBSUB_AUDIENCE=https://your-domain.com/api/webhooks/gchat # For Pub/Sub JWT verification
# GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true # Escape hatch for local dev only

Expand All @@ -213,23 +215,34 @@ The adapter supports JWT verification for both webhook types. When configured, t

Verification is required. The constructor throws `ValidationError` unless one of the following is set:

- `googleChatProjectNumber` (or `GOOGLE_CHAT_PROJECT_NUMBER`) — direct webhooks
- `googleChatProjectNumber` (or `GOOGLE_CHAT_PROJECT_NUMBER`) — direct webhooks when the Chat app's authentication audience is "Project number"
- `endpointUrl` — direct webhooks when the Chat app's authentication audience is "HTTP endpoint URL"; also required for routing card button clicks in HTTP endpoint apps
- `pubsubAudience` (or `GOOGLE_CHAT_PUBSUB_AUDIENCE`) — Pub/Sub push deliveries
- `disableSignatureVerification: true` (or `GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true`) — explicit opt-out, intended for local development only

The two transports share one HTTP endpoint, so each verifier only covers its own request shape. If you only configure `googleChatProjectNumber`, incoming Pub/Sub-shaped requests are rejected with HTTP 401, and vice versa — configure both if you receive both.
The two transports share one HTTP endpoint, so each verifier only covers its own request shape. If you only configure a direct-webhook verifier, incoming Pub/Sub-shaped requests are rejected with HTTP 401, and vice versa — configure both if you receive both.

### Direct webhooks (Google Chat API)

Google Chat sends a signed JWT with every webhook request. The JWT audience (`aud` claim) is your GCP project number.
Google Chat sends a signed token with every webhook request. The expected JWT audience (`aud` claim) depends on the Chat app's **Authentication audience** setting:

```typescript
createGoogleChatAdapter({
googleChatProjectNumber: "123456789",
});
```

Find your project number in the [GCP Console dashboard](https://console.cloud.google.com/home/dashboard) (it's different from the project ID).
Use `googleChatProjectNumber` when the setting is **Project number**. Find your project number in the [GCP Console dashboard](https://console.cloud.google.com/home/dashboard) (it's different from the project ID).

```typescript
createGoogleChatAdapter({
endpointUrl: "https://your-domain.com/api/webhooks/gchat",
});
```

Use `endpointUrl` when the setting is **HTTP endpoint URL**. Workspace Add-on Chat apps always use URL audience tokens. When both `googleChatProjectNumber` and `endpointUrl` are set, either token type is accepted.

The two modes carry different token types, and the adapter verifies each per [Google's reference implementation](https://developers.google.com/workspace/chat/verify-requests-from-chat): endpoint-URL tokens are standard Google OIDC ID tokens (checked against Google's public certs, plus the Chat service-account `email` claim with `email_verified`), while project-number tokens are JWTs self-signed by `chat@system.gserviceaccount.com` (checked against that service account's X.509 certificates with issuer `chat@system.gserviceaccount.com`). The configured `endpointUrl` must exactly match the URL registered in the Chat API console — a trailing-slash or scheme mismatch fails verification.

### Pub/Sub push messages

Expand Down
Loading