DEV Community

Siarhei Hlasouski
Siarhei Hlasouski

Posted on

10 3

Verifying Google Chat request in NodeJS

Google Chat includes a bearer token in the Authorization header of every HTTPS Request to a bot. For example:

POST
Host: yourboturl.com
Authorization: Bearer %JWT%
Content-Type: application/json
User-Agent: Google-Dynamite
Enter fullscreen mode Exit fullscreen mode

Decoded JWT token by jwt.io
Header:

{
  "alg": "RS256",
  "kid": "424189bd9a18927f7ee924f1a100601f1524f441",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

Payload:

{
  "aud": "1234567890",
  "exp": 1629047990,
  "iat": 1629044390,
  "iss": "chat@system.gserviceaccount.com"
}
Enter fullscreen mode Exit fullscreen mode

All bearer tokens sent with requests from Google chat will have chat@system.gserviceaccount.com as the issuer, with the audience field specifying the target bot's project number from the Google API Console. For example, if the request is for a bot with the project number 1234567890, then the audience is 1234567890. [Verifying bot authenticity]

  1. Extract the KID from the header: 424189bd9a18927f7ee924f1a100601f1524f441
  2. Use the KID to find the matching public key in the JWKS (JSON Web Key Set) endpoint https://www.googleapis.com/service_accounts/v1/jwk/chat@system.gserviceaccount.com
  3. Verify JWT token using corresponded public key and passing audience and issuer options.

Complete solution

Dependencies

import { NextFunction, Request, Response, Router } from 'express';
import jwt from 'jsonwebtoken';
import { JwksClient } from 'jwks-rsa';

const GOOGLE_CHAT_PROJECT_NUMBER = '1234567890';

const jwksClient = new JwksClient({
  jwksUri:
    'https://www.googleapis.com/service_accounts/v1/jwk/chat@system.gserviceaccount.com',
  cache: true,
});

const router: Router = Router();

router.post('/google-chat/events', verificationRequestMiddleware(), async (req, res) => {
  // process google chat event
});

function verificationRequestMiddleware() {
  return async (request: Request, response: Response, next: NextFunction) => {
    const isVerified = await verifyRequest(request);

    if (!isVerified) {
      throw new UnauthorizedError('Authentication failed');
    }

    return next();
  };
}

async function verifyRequest(request: Request): Promise<boolean> {
  const prefix = 'Bearer ';
  const authHeader = request.header('Authorization') as string;
  const token = authHeader?.startsWith(prefix) ? authHeader.slice(prefix.length) : null;

  if (!token) {
    return false;
  }

  return new Promise<boolean>((resolve, reject) => {
    const getKey = (header, callback) => {
      jwksClient.getSigningKey(header.kid, (err, key) => {
        const signingKey = key.getPublicKey();
        callback(null, signingKey);
      });
    };

    jwt.verify(
      token,
      getKey,
      {
        audience: GOOGLE_CHAT_PROJECT_NUMBER,
        issuer: 'chat@system.gserviceaccount.com'
      },
      (err: any, decoded: any) => {
        if (err) {
          reject(false);
        } else {
          resolve(true);
        }
      }
    );
  });   
}
Enter fullscreen mode Exit fullscreen mode

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
julbrs profile image
Julien Bras

Thanks for that 🙏
I was struggling with this developers.google.com/chat/api/gui...
There is no Node example 😅

I have been able to reuse your example for my project!

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay