Skip to content
Open
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
27 changes: 27 additions & 0 deletions .changeset/gchat-endpoint-url-audience.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@chat-adapter/gchat": patch
---

fix(gchat): accept `endpointUrl` as a direct-webhook JWT audience

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 JWTs have `aud` equal to 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 verifies the bearer token against
`googleChatProjectNumber` and/or `endpointUrl`, accepting either when both
are set, and the constructor's fail-closed check accepts `endpointUrl` as a
valid direct-webhook verifier.

Direct-webhook tokens are now also matched against the expected 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`,
so a public endpoint URL audience alone is not sufficient to forge a request.

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, because `request.url` derives from the
attacker-controllable `Host` header in serverless runtimes.
28 changes: 24 additions & 4 deletions apps/docs/content/adapters/official/google-chat.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 | JWT `aud` | JWT `email` | Set |
| -------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Standalone Chat app, **Authentication audience: Project number** (in Chat API config) | project number | `chat@system.gserviceaccount.com` | `googleChatProjectNumber` |
| Standalone Chat app, **Authentication audience: HTTP endpoint URL** | endpoint URL | `chat@system.gserviceaccount.com` | `endpointUrl` |
| **Workspace Add-on Chat app** (built via Google Workspace Marketplace SDK; the audience is hardcoded) | endpoint URL | `service-{projectNumber}@gcp-sa-gsuiteaddons.iam.gserviceaccount.com` | `endpointUrl` |
| Mixed across envs / not sure | varies | varies | both `googleChatProjectNumber` and `endpointUrl` |

When both `googleChatProjectNumber` and `endpointUrl` are set, either audience is accepted. If you don't know which mode your app uses, look at the JWT `email` claim of an incoming request — `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
29 changes: 20 additions & 9 deletions packages/adapter-gchat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,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 @@ -171,7 +173,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 @@ -182,8 +184,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 @@ -197,23 +199,32 @@ 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 audience is accepted.

### Pub/Sub push messages

Expand Down
Loading