如何使用 Deno 验证 GitHub webhook?

How to validate GitHub webhook with Deno?

我正在尝试使用 Deno 创建一个 GitHub webhook 服务器,但我找不到任何可能的方法来进行验证。

这是我目前尝试使用 webhooks-methods.js:

import { Application } from "https://deno.land/x/oak/mod.ts";
import { verify } from "https://cdn.skypack.dev/@octokit/webhooks-methods?dts";

const app = new Application();

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (_err) {
    ctx.response.status = 500;
  }
});

const secret = "...";

app.use(async (ctx) => {
  const signature = ctx.request.headers.get("X-Hub-Signature-256");
  if (signature) {
    const payload = await ctx.request.body({ type: "text" }).value;
    const result = await verify(secret, payload, signature);
    console.log(result);
  }
  ctx.response.status = 200;
});

verify 函数每次都返回 false

你的例子非常接近。 GitHub webhook documentation 详细说明了签名 header 架构。值为摘要算法前缀,后跟签名,格式为${ALGO}=${SIGNATURE}:

X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c

因此,您需要从值中提取签名(省略前缀):

const signatureHeader = request.headers.get('X-Hub-Signature-256');
const signature = signatureHeader.slice('sha256='.length);

这是一个完整的工作示例,您可以简单地将其复制并粘贴到 Deno Deploy 上的 playground 或项目中:

gh-webhook-logger.ts:

import {assert} from 'https://deno.land/std@0.132.0/testing/asserts.ts';
import {Application, Router, NativeRequest} from 'https://deno.land/x/oak@v10.5.1/mod.ts';
import type {ServerRequest} from 'https://deno.land/x/oak@v10.5.1/types.d.ts';
import {verify} from 'https://raw.githubusercontent.com/octokit/webhooks-methods.js/v2.0.0/src/web.ts';

// In actual usage, use a private secret:
// const SECRET = Deno.env.get('SIGNING_SECRET');

// But for the purposes of this demo, the exposed secret is:
const SECRET = 'Let me know if you found this to be helpful!';

type GitHubWebhookVerificationStatus = {
  id: string;
  verified: boolean;
}

// Because this uses a native Request, it can be used in other contexts besides Oak (e.g. `std/http/serve`)
async function verifyGitHubWebhook (request: Request): Promise<GitHubWebhookVerificationStatus> {
  const id = request.headers.get('X-GitHub-Delivery');

  // This should be more strict in reality
  if (!id) throw new Error('Not a GH webhhok');

  const signatureHeader = request.headers.get('X-Hub-Signature-256');
  let verified = false;
  if (signatureHeader) {
    const signature = signatureHeader.slice('sha256='.length);
    const payload = await request.clone().text();
    verified = await verify(SECRET, payload, signature);
  }
  return {id, verified};
}

// Type predicate used to access native Request instance
// Ref: https://github.com/oakserver/oak/issues/501#issuecomment-1084046581
function isNativeRequest (r: ServerRequest): r is NativeRequest {
  // deno-lint-ignore no-explicit-any
  return (r as any).request instanceof Request;
}

const webhookLogger = new Router().post('/webhook', async (ctx) => {
  assert(isNativeRequest(ctx.request.originalRequest));
  const status = await verifyGitHubWebhook(ctx.request.originalRequest.request);
  console.log(status);
  ctx.response.status = 200;
});

const app = new Application()
  .use(webhookLogger.routes())
  .use(webhookLogger.allowedMethods());

// The port is not important in Deno Deploy
await app.listen({port: 8080});