Skip to content

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

Merged
bensabic merged 4 commits into
vercel:mainfrom
mdnanocom:fix/gchat-endpoint-url-audience
Jul 15, 2026
Merged

fix(gchat): accept endpointUrl as a direct-webhook JWT audience#518
bensabic merged 4 commits into
vercel:mainfrom
mdnanocom:fix/gchat-endpoint-url-audience

Conversation

@mdnanocom

@mdnanocom mdnanocom commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

When a Google Chat app's connection setting Authentication audience is set to HTTP endpoint URL (Google's recommended option for HTTP-hosted apps that aren't behind Cloud Run IAM, see Verify requests from Google Chat),
the bearer token Google sends is an OIDC ID token whose aud is the endpoint URL — not the GCP project number.
The adapter previously only verified against googleChatProjectNumber, so URL-audience tokens always failed with
401 Unauthorized and direct webhooks silently broke for any app configured this way.

This change makes the adapter verify direct-webhook JWTs against googleChatProjectNumber and/or endpointUrl, accepting either when both are configured (handy for multi-env setups that mix the two modes).
The constructor's fail-closed check accepts an explicit endpointUrl as a valid direct-webhook verifier alongside googleChatProjectNumber, pubsubAudience, and disableSignatureVerification.

Behavior

Config Direct-webhook aud accepted
googleChatProjectNumber only (current behavior) project number
endpointUrl only (new) endpoint URL
Both googleChatProjectNumber and endpointUrl (new) either
Neither, no pubsubAudience, no disableSignatureVerification constructor throws (unchanged)

Security note

Auto-detected endpoint URLs (the value handleWebhook falls back to from the incoming request.url when endpointUrl is not configured) are intentionally not promoted to verifier status. Treating an auto-detected URL as a valid audience would let any caller bypass verification by hitting the bot at a URL of their choice.
A new endpointUrlIsAudience flag captures whether the caller explicitly configured endpointUrl, and only that case enables URL-based verification. A regression test guards this.

Implementation

  • verifyBearerToken accepts string | string[] (OAuth2Client.verifyIdToken
    already supports both).
  • handleWebhook builds directAudiences = [projectNumber, explicitEndpointUrl].filter(Boolean) and passes a single string when only one verifier is configured (to preserve the prior call shape) or an array when both are.
  • Docs updated in apps/docs/content/adapters/official/google-chat.mdx: describe both authentication-audience modes and document endpointUrl as an accepted verifier.
  • Changeset: @chat-adapter/gchat: patch.

Test plan

  • pnpm --filter @chat-adapter/gchat test — 250/250 pass (5 new tests).
  • pnpm typecheck — 33/33 tasks pass.
  • pnpm check (Ultracite/Biome) clean.
  • pnpm konsistent clean.
  • Validated end-to-end against a real Google Chat app configured with
    "HTTP endpoint URL" as authentication audience: webhooks that previously
    401'd now verify and process correctly.

@mdnanocom mdnanocom requested a review from a team as a code owner May 18, 2026 08:52
@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@mdnanocom is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@mdnanocom mdnanocom force-pushed the fix/gchat-endpoint-url-audience branch 2 times, most recently from 62ad0bb to e01684a Compare May 18, 2026 09:09
mdnanocom and others added 4 commits July 15, 2026 12:50
When a Google Chat app is configured with **HTTP endpoint URL** as its
authentication audience (the recommended option for HTTP-hosted apps not
behind Cloud Run IAM), Google issues OIDC tokens whose `aud` is the
endpoint URL rather than the GCP project number. The adapter previously
only verified against `googleChatProjectNumber`, so URL-audience tokens
always 401'd.

Verify the bearer token against `googleChatProjectNumber` and/or an
explicitly-configured `endpointUrl`, accepting either when both are set.
The constructor's fail-closed check now also accepts an explicit
`endpointUrl` as a valid direct-webhook verifier. Auto-detected endpoint
URLs (populated from the request URL inside `handleWebhook`) are
intentionally NOT promoted to verifier status — that would let a caller
hitting the bot at any URL bypass verification.

Tests cover the URL-audience-only path, the both-audiences path, and
the auto-detected-URL-must-not-bypass-verification regression.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ensure URL-audience Google Chat webhooks come from the expected Chat service identities, and document endpointUrl setup in the package README.

Co-authored-by: Cursor <cursoragent@cursor.com>
Auto-detect the public endpoint URL from incoming requests so simple deployments don't need to hardcode it, but keep the inferred value strictly out of the direct-webhook JWT audience set so a spoofed Host header can't influence verification.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nfer endpoint URL only after verification

Two fixes to the direct-webhook verification introduced in this PR:

1. Project-number-audience tokens are self-signed JWTs from
   chat@system.gserviceaccount.com, not OIDC ID tokens. verifyIdToken can
   never validate them: it checks Google's OIDC federated certs and only
   accepts accounts.google.com issuers, so the previous iss claim check was
   unreachable and real project-number tokens always failed. Verify them per
   Google's reference implementation instead: verifySignedJwtWithCertsAsync
   against the Chat service account's X.509 certs (cached 1h) with issuer
   chat@system.gserviceaccount.com. Endpoint-URL tokens keep the OIDC
   verifyIdToken path plus email/email_verified claim checks. When both
   verifiers are configured, either token type is accepted.

2. Move button-click endpoint URL inference after successful verification.
   Previously an unauthenticated request (401) could still poison
   inferredEndpointUrl via a spoofed Host header, first-write-wins, and the
   poisoned URL flowed into every outbound card's action routing.

Adds regression tests for both; updates docs, README, and the changeset.
@bensabic bensabic force-pushed the fix/gchat-endpoint-url-audience branch from 0d93d95 to 923260c Compare July 15, 2026 02:53
@bensabic bensabic merged commit 270b1c2 into vercel:main Jul 15, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants