Search code examples
javascriptgithubdeno

How to validate GitHub webhook with Deno?


I'm trying to make a GitHub webhook server with Deno, but I cannot find any possible way to do the validation.

This is my current attempt using 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;
});

The verify function is returning false every time.


Solution

  • Your example is very close. The GitHub webhook documentation details the signature header schema. The value is a digest algorithm prefix followed by the signature, in the format of ${ALGO}=${SIGNATURE}:

    X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
    

    So, you need to extract the signature from the value (omitting the prefix):

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

    Update: Starting in release version 3.0.1 of octokit/webhooks-methods.js, it is no longer necessary to manually extract the signature from the header — that task is handled by the verify function. The code in the answer has been updated to reflect this change.


    Here's a complete, working example that you can simply copy + paste into a project or playground on Deno Deploy:

    gh-webhook-logger.ts:

    import { assert } from "https://deno.land/std@0.177.0/testing/asserts.ts";
    
    import {
      Application,
      NativeRequest,
      Router,
    } from "https://deno.land/x/oak@v11.1.0/mod.ts";
    
    import type { ServerRequest } from "https://deno.land/x/oak@v11.1.0/types.d.ts";
    
    import { verify } from "https://esm.sh/@octokit/webhooks-methods@3.0.2?pin=v106";
    
    // 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
      assert(id, "Not a GH webhhok");
    
      const signatureHeader = request.headers.get("X-Hub-Signature-256");
      let verified = false;
    
      if (signatureHeader) {
        const payload = await request.clone().text();
        verified = await verify(SECRET, payload, signatureHeader);
      }
    
      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 });