Issue Summary
Signature mismatch on webhook
Steps to Reproduce
- Create a new mock server on postman. E.g. https://8e11246e-2511-4b8d-a236-bb6e90712ccd.mock.pstmn.io
- Create a new webhook on Ghost, with secret = “test123” and event “post.published”
- Publish a new post from the Ghost interface
- Check the logs from the postman mockup server to retrieve the calculated signature and the json body
- Run node.js code to recalculate signature with the secret and verify they are mismatched
Setup information
Ghost Version
Hosted on ghost.org
Relevant log / error output
Example log from Postman
x-ghost-signature: sha256=6d30fe32b3128f5370936aa10e0fc3b9fe4e9b0ebf428bc9378f4f24f501f20c, t=1732539451946
"
body:
{"post":{"current":{"id":"67447438e61fb200012f48e0","uuid":"001cdb2b-98bf-4b05-b46c-15b3b93cade0","title":"test","slug":"test","mobiledoc":null,"html":"<p>test</p>","comment_id":"67447438e61fb200012f48e0","plaintext":"test","feature_image":null,"featured":false,"status":"published","visibility":"public","created_at":"2024-11-25T12:57:28.000Z","updated_at":"2024-11-25T12:57:31.000Z","published_at":"2024-11-25T12:57:31.000Z","custom_excerpt":null,"codeinjection_head":null,"codeinjection_foot":null,"custom_template":null,"canonical_url":null,"authors":[{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z","roles":[{"id":"6743f1c2e207cd00088541fd","name":"Owner","description":"Blog Owner","created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z"}],"url":"https://lnk-bio.ghost.io/author/lnk/"}],"tags":[],"post_revisions":[{"id":"67447438e61fb200012f48e2","post_id":"67447438e61fb200012f48e0","lexical":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"t\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","created_at_ts":1732539448357,"created_at":"2024-11-25T12:57:28.000Z","title":"test","post_status":"draft","reason":"initial_revision","feature_image":null,"feature_image_alt":null,"feature_image_caption":null,"custom_excerpt":null,"author":{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","locale":null,"visibility":"public","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z"}},{"id":"6744743be61fb200012f48e5","post_id":"67447438e61fb200012f48e0","lexical":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"test\",\"type\":\"extended-text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","created_at_ts":1732539451878,"created_at":"2024-11-25T12:57:31.000Z","title":"test","post_status":"published","reason":"published","feature_image":null,"feature_image_alt":null,"feature_image_caption":null,"custom_excerpt":null}],"tiers":[{"id":"6743f1c2e207cd0008854206","name":"Free","slug":"free","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"free","currency":null,"monthly_price":null,"yearly_price":null,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null},{"id":"6743f1c2e207cd0008854207","name":"Lnk.Bio","slug":"default-product","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"paid","currency":"usd","monthly_price":500,"yearly_price":5000,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null}],"count":{"clicks":0,"positive_feedback":0,"negative_feedback":0},"primary_author":{"id":"1","name":"Lnk.Bio","slug":"lnk","email":"info@lnk.bio","profile_image":"https://www.gravatar.com/avatar/36927dbfe7c60a2fe221af9e66d42c60?s=250&r=x&d=mp","cover_image":null,"bio":null,"website":null,"location":null,"facebook":null,"twitter":null,"accessibility":"{\"onboarding\":{\"completedSteps\":[],\"checklistState\":\"started\"}}","status":"active","meta_title":null,"meta_description":null,"tour":null,"last_seen":"2024-11-25T12:04:11.000Z","comment_notifications":true,"free_member_signup_notification":true,"paid_subscription_started_notification":true,"paid_subscription_canceled_notification":false,"mention_notifications":true,"recommendation_notifications":true,"milestone_notifications":true,"donation_notifications":true,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T12:04:12.000Z","roles":[{"id":"6743f1c2e207cd00088541fd","name":"Owner","description":"Blog Owner","created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z"}],"url":"https://lnk-bio.ghost.io/author/lnk/"},"primary_tag":null,"email_segment":"all","url":"https://lnk-bio.ghost.io/test/","excerpt":"test","reading_time":0,"og_image":null,"og_title":null,"og_description":null,"twitter_image":null,"twitter_title":null,"twitter_description":null,"meta_title":null,"meta_description":null,"email_subject":null,"frontmatter":null,"feature_image_alt":null,"feature_image_caption":null,"email_only":false},"previous":{"status":"draft","updated_at":"2024-11-25T12:57:28.000Z","html":"<p>t</p>","plaintext":"test","published_at":null,"tiers":[{"id":"6743f1c2e207cd0008854206","name":"Free","slug":"free","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"free","currency":null,"monthly_price":null,"yearly_price":null,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null},{"id":"6743f1c2e207cd0008854207","name":"Lnk.Bio","slug":"default-product","active":true,"welcome_page_url":null,"visibility":"public","trial_days":0,"description":null,"type":"paid","currency":"usd","monthly_price":500,"yearly_price":5000,"created_at":"2024-11-25T03:40:50.000Z","updated_at":"2024-11-25T03:40:50.000Z","monthly_price_id":null,"yearly_price_id":null}]}}}
example signature recalculation
const crypto = require(‘crypto’);
const secret = ‘test123’;
const payload = ‘’ // the post from above
signature = crypto.createHmac(‘sha256’, secret).update(payload, ‘utf8’).digest(‘hex’);
console.log(Signature: ${signature}
);
generated signature:
37e4b1fc2ee4e8a64242607628f988ba0c7524c45f318799f5a131c8b76f7132
So the difference is between
37e4b1fc2ee4e8a64242607628f988ba0c7524c45f318799f5a131c8b76f7132 (recalculated)
6d30fe32b3128f5370936aa10e0fc3b9fe4e9b0ebf428bc9378f4f24f501f20c (from header)
I don’t see any logic issue in how the sha256 is generated and the body is the exact one provided by ghost.
See also setup image from the webhook panel