mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 04:54:25 -07:00
feat: add JWT verification middleware for Google Chat webhook
Verifies Bearer tokens from Google Chat using google-auth-library. Checks issuer (chat@system.gserviceaccount.com) and audience (CHAT_PROJECT_NUMBER). Verification is skipped when project number is not configured, allowing local testing without tokens.
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"express": "^5.1.0",
|
||||
"google-auth-library": "^9.11.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"tar": "^7.5.2",
|
||||
"uuid": "^13.0.0",
|
||||
|
||||
@@ -7,46 +7,110 @@
|
||||
/**
|
||||
* Express routes for the Google Chat bridge webhook.
|
||||
* Adds a POST /chat/webhook endpoint to the existing Express app.
|
||||
* Includes JWT verification for Google Chat requests when configured.
|
||||
*/
|
||||
|
||||
import type { Router, Request, Response } from 'express';
|
||||
import type { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Router as createRouter } from 'express';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import type { ChatEvent, ChatBridgeConfig } from './types.js';
|
||||
import { ChatBridgeHandler } from './handler.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';
|
||||
|
||||
/**
|
||||
* Creates middleware that verifies Google Chat JWT tokens.
|
||||
* When projectNumber is set, requests must include a valid Bearer token
|
||||
* signed by Google Chat with the correct audience.
|
||||
* When not set, verification is skipped (for local testing).
|
||||
*/
|
||||
function createAuthMiddleware(
|
||||
projectNumber: string | undefined,
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
if (!projectNumber) {
|
||||
logger.warn(
|
||||
'[ChatBridge] CHAT_PROJECT_NUMBER not set — JWT verification disabled. ' +
|
||||
'Set it in production to verify requests come from Google Chat.',
|
||||
);
|
||||
return (_req: Request, _res: Response, next: NextFunction) => {
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
const authClient = new OAuth2Client();
|
||||
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
logger.warn('[ChatBridge] Missing or invalid Authorization header');
|
||||
res.status(401).json({ error: 'Unauthorized: missing Bearer token' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
authClient
|
||||
.verifyIdToken({
|
||||
idToken: token,
|
||||
audience: projectNumber,
|
||||
})
|
||||
.then((ticket) => {
|
||||
const payload = ticket.getPayload();
|
||||
if (payload?.iss !== CHAT_ISSUER) {
|
||||
logger.warn(
|
||||
`[ChatBridge] Invalid token issuer: ${payload?.iss ?? 'unknown'}`,
|
||||
);
|
||||
res.status(403).json({ error: 'Forbidden: invalid token issuer' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error';
|
||||
logger.warn(`[ChatBridge] Token verification failed: ${msg}`);
|
||||
res.status(401).json({ error: 'Unauthorized: invalid token' });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Express routes for the Google Chat bridge.
|
||||
*/
|
||||
export function createChatBridgeRoutes(config: ChatBridgeConfig): Router {
|
||||
const router = createRouter();
|
||||
const handler = new ChatBridgeHandler(config);
|
||||
const authMiddleware = createAuthMiddleware(config.projectNumber);
|
||||
|
||||
// Google Chat sends webhook events as POST requests
|
||||
router.post('/chat/webhook', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const event = req.body as ChatEvent;
|
||||
router.post(
|
||||
'/chat/webhook',
|
||||
authMiddleware,
|
||||
async (req: Request, res: Response) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const event = req.body as ChatEvent;
|
||||
|
||||
if (!event || !event.type) {
|
||||
res.status(400).json({ error: 'Invalid event: missing type field' });
|
||||
return;
|
||||
if (!event || !event.type) {
|
||||
res.status(400).json({ error: 'Invalid event: missing type field' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`[ChatBridge] Webhook received: type=${event.type}`);
|
||||
|
||||
const response = await handler.handleEvent(event);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const errorMsg =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error(`[ChatBridge] Webhook error: ${errorMsg}`, error);
|
||||
res.status(500).json({
|
||||
text: `Internal error: ${errorMsg}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
logger.info(`[ChatBridge] Webhook received: type=${event.type}`);
|
||||
|
||||
const response = await handler.handleEvent(event);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error(`[ChatBridge] Webhook error: ${errorMsg}`, error);
|
||||
res.status(500).json({
|
||||
text: `Internal error: ${errorMsg}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint for the chat bridge
|
||||
// Health check endpoint for the chat bridge (no auth required)
|
||||
router.get('/chat/health', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
||||
@@ -314,6 +314,7 @@ export async function createApp() {
|
||||
if (chatBridgeUrl) {
|
||||
const chatRoutes = createChatBridgeRoutes({
|
||||
a2aServerUrl: chatBridgeUrl,
|
||||
projectNumber: process.env['CHAT_PROJECT_NUMBER'],
|
||||
debug: process.env['CHAT_BRIDGE_DEBUG'] === 'true',
|
||||
});
|
||||
expressApp.use(chatRoutes);
|
||||
|
||||
Reference in New Issue
Block a user