Webhook signature validation constantly failing (Always getting 401 Unauthorized) #199399
-
🏷️ Discussion TypeBug 💬 Feature/Topic AreaAPI BodyHey everyone, I’m trying to set up a GitHub Webhook to listen for repository events and trigger an internal sync script on my Node.js/Express backend. To keep it secure, I added a Webhook Secret in the GitHub repository settings. However, no matter what I try, the signature verification on my server constantly fails. I keep getting a 401 because the hash I calculate locally never matches the x-hub-signature-256 header sent by GitHub. Here is the logic I'm using to verify it app.post('/webhook', (req, res) => { const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex'); if (signature === digest) { |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
|
💬 Your Product Feedback Has Been Submitted 🎉 Thank you for taking the time to share your insights with us! Your feedback is invaluable as we build a better GitHub experience for all our users. Here's what you can expect moving forward ⏩
Where to look to see what's shipping 👀
What you can do in the meantime 💻
As a member of the GitHub community, your participation is essential. While we can't promise that every suggestion will be implemented, we want to emphasize that your feedback is instrumental in guiding our decisions and priorities. Thank you once again for your contribution to making GitHub even better! We're grateful for your ongoing support and collaboration in shaping the future of our platform. ⭐ |
Beta Was this translation helpful? Give feedback.
-
|
Ah, welcome to the absolute rite of passage that is webhook signature validation. Almost everyone gets burned by this exact issue the first time. The culprit is almost certainly this line right here: JSON.stringify(req.body). The Problem: When you try to turn it back into a string using JSON.stringify(req.body), the formatting (like spacing, indentation, or property order) changes ever so slightly compared to the exact raw payload GitHub sent. Crypto hashing is incredibly strict—if even a single character or whitespace is off, the signatures won't match. You must compute the hash using the exact raw, unparsed request body text. The Fix Update your middleware and route setup to look like this: // 1. Capture the raw body buffer during parsing app.post('/webhook', (req, res) => { // 2. Use the raw body buffer to calculate the HMAC // 3. Use timingSafeEqual to prevent timing attacks if (crypto.timingSafeEqual(trusted, untrusted)) { crypto.timingSafeEqual: I added this to the fix above. Using a standard === string comparison exposes your endpoint to timing attacks. It's a best practice to use a constant-time comparison helper when dealing with security hashes. Next.js Note: If you ever migrate this endpoint to a Next.js API route or App Router handler, you'll hit the same wall. Instead of req.body, make sure to use await req.text() to get the raw body string directly from the incoming web request object. Switch over to using the raw body buffer and your webhook test deliveries should instantly go green! |
Beta Was this translation helpful? Give feedback.
Ah, welcome to the absolute rite of passage that is webhook signature validation. Almost everyone gets burned by this exact issue the first time.
The culprit is almost certainly this line right here: JSON.stringify(req.body).
The Problem:
If you are using standard middleware like app.use(express.json()) earlier in your code, Express parses the incoming raw string from GitHub and turns it into a JavaScript object (req.body).
When you try to turn it back into a string using JSON.stringify(req.body), the formatting (like spacing, indentation, or property order) changes ever so slightly compared to the exact raw payload GitHub sent. Crypto hashing is incredibly strict—if even a single charact…