Skip to content

Instantly share code, notes, and snippets.

@stigok
Last active July 30, 2023 09:46
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save stigok/57d075c1cf2a609cb758898c0b202428 to your computer and use it in GitHub Desktop.
Save stigok/57d075c1cf2a609cb758898c0b202428 to your computer and use it in GitHub Desktop.
Verify GitHub webhook signature header in Node.js
/*
* Verify GitHub webhook signature header in Node.js
* Written by stigok and others (see gist link for contributor comments)
* https://gist.github.com/stigok/57d075c1cf2a609cb758898c0b202428
* Licensed CC0 1.0 Universal
*/
const crypto = require('crypto')
const express = require('express')
const bodyParser = require('body-parser')
const secret = CHANGE_ME;
// For these headers, a sigHashAlg of sha1 must be used instead of sha256
// GitHub: X-Hub-Signature
// Gogs: X-Gogs-Signature
const sigHeaderName = 'X-Hub-Signature-256'
const sigHashAlg = 'sha256'
const app = express()
// Saves a valid raw JSON body to req.rawBody
// Credits to https://stackoverflow.com/a/35651853/90674
app.use(bodyParser.json({
verify: (req, res, buf, encoding) => {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8');
}
},
}))
function verifyPostData(req, res, next) {
if (!req.rawBody) {
return next('Request body empty')
}
const sig = Buffer.from(req.get(sigHeaderName) || '', 'utf8')
const hmac = crypto.createHmac(sigHashAlg, secret)
const digest = Buffer.from(sigHashAlg + '=' + hmac.update(req.rawBody).digest('hex'), 'utf8')
if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
return next(`Request body digest (${digest}) did not match ${sigHeaderName} (${sig})`)
}
return next()
}
app.post('/', verifyPostData, function (req, res) {
res.status(200).send('Request body was signed')
})
app.use((err, req, res, next) => {
if (err) console.error(err)
res.status(403).send('Request body was not signed or verification failed')
})
app.listen(3000, () => console.log("Listening on port 3000"))
@jboucly
Copy link

jboucly commented Nov 23, 2022

Thanks a lot !!!!

@rgil90
Copy link

rgil90 commented Nov 30, 2022

Does the request body get sent as a base64 encoded string? I'm trying to write an AWS lambda function to deal with this. Not sure if this is something that happens with AWS in general or if the payload actually gets sent as a base64 encoded string.

@stigok
Copy link
Author

stigok commented Dec 1, 2022

Does the request body get sent as a base64 encoded string? I'm trying to write an AWS lambda function to deal with this. Not sure if this is something that happens with AWS in general or if the payload actually gets sent as a base64 encoded string.

I recommend you read the API documentation for AWS lambda to find an answer to this. This code works as-is for incoming HTTP GitHub webhook requests.

@untuned
Copy link

untuned commented Dec 20, 2022

I'm trying to figure this out while using Cloudflare Workers (and itty-router instead of express), anyone have any suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment