Closed cmnstmntmn closed 2 years ago
Add middleware to each post request for Webhooks to validate the hmac.
Webhooks
like this?
const webhook = receiveWebhook({ secret: SHOPIFY_API_SECRET_KEY, })
// GDPR Webhooks router.post('/webhooks/customers/redact', webhook, async ctx => { ctx.res.statusCode = 200 }) router.post('/webhooks/shop/redact', webhook, async ctx => { ctx.res.statusCode = 200 }) router.post('/webhooks/customers/data_request', webhook, async ctx => { ctx.res.statusCode = 200 })
make it a proper function for hmac validation.
make it a proper function for hmac validation.
require('isomorphic-fetch') const Koa = require('koa') // const helmet = require("koa-helmet") const { koaBody } = require('koa-body')
const { Shopify } = require('@shopify/shopify-api')
const next = require('next') const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth') const { verifyRequest } = require('@shopify/koa-shopify-auth') const session = require('koa-session') const Router = require('koa-router') const getSubscriptionUrl = require('./utils/getSubscriptionUrl')
const { APP_CONFIG } = require('./config/app.config') const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY, PORT } = APP_CONFIG
const port = parseInt(PORT, 10) const dev = process.env.NODE_ENV !== 'production' const app = next({ dev, }) const handle = app.getRequestHandler()
app.prepare().then(() => { const server = new Koa() const router = new Router() server.use( session( { sameSite: 'none', secure: true, }, server ) ) server.keys = [SHOPIFY_API_SECRET_KEY]
server.use( createShopifyAuth({ apiKey: SHOPIFY_API_KEY, secret: SHOPIFY_API_SECRET_KEY, scopes: ['read_products', 'read_orders'], async afterAuth(ctx) { const { shop, accessToken } = ctx.session ctx.cookies.set('shopOrigin', shop, { httpOnly: false, secure: true, sameSite: 'none', })
ctx.cookies.set('accessToken', accessToken, {
httpOnly: false,
secure: true,
sameSite: 'none',
})
await getSubscriptionUrl(ctx, accessToken, shop)
const response = await fetch(
`https://${shop}/admin/api/2019-07/shop.json`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': accessToken,
},
}
)
const shopId = response.headers._headers['x-shopid']
ctx.cookies.set('shopId', shopId, {
httpOnly: false,
secure: true,
sameSite: 'none',
})
},
})
)
function verifyWebhookRequest(body) { try { const generatedHash = crypto .createHmac("SHA256", Shopify.Context.API_SECRET_KEY) .update(JSON.stringify(body), "utf8") .digest("base64"); const hmac = req.get(ShopifyHeader.Hmac); // Equal to 'X-Shopify-Hmac-Sha256' at time of coding const safeCompareResult = Shopify.Utils.safeCompare(generatedHash, hmac); if (!!safeCompareResult) { return true; } else { return false; } } catch (error) { return false; } }
// GDPR Webhooks router.post('/webhooks/customers/redact', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } }) router.post('/webhooks/shop/redact', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } }) router.post('/webhooks/customers/data_request', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } })
/**
{@step} form/email/status
*/
router.get('/complete/:step?', async ctx => {
ctx.res.statusCode = 200
const { step } = ctx.params
await app.render(ctx.req, ctx.res, /complete/${step}
, {
step,
...ctx.query,
})
})
// error
router.get('/error', async ctx => {
ctx.res.statusCode = 200
await app.render(ctx.req, ctx.res, /_error
, {
...ctx.query,
})
})
router.get('/auth/null', async ctx => { ctx.res.statusCode = 200 ctx.response.redirect('/'); })
router.get('*', verifyRequest(), async ctx => { await handle(ctx.req, ctx.res) ctx.respond = false ctx.res.statusCode = 200 })
// server.use(helmet({
// frameguard: false
// })) // X-Frame-Options // 允许iframe
server.use(router.allowedMethods())
server.use(router.routes())
server.listen(port, () => {
console.log(> Ready on http://localhost:${port}
)
})
})
post request to "https://686d-180-169-108-50.ngrok.io/webhooks/customers/data_request" and response Unauthorized
import crypto from "crypto"; import { Shopify } from "@shopify/shopify-api";
export const hmacVerify = (req, res, next) => { try { const generateHash = crypto .createHmac("SHA256", process.env.SHOPIFY_API_SECRET) .update(JSON.stringify(req.body), "utf8") .digest("base64");
const hmac = req.headers["x-shopify-hmac-sha256"];
if (Shopify.Utils.safeCompare(generateHash, hmac)) {
console.log("HMAC successfully verified for webhook route.");
next();
} else {
console.log("Shopify hmac verification for webhook failed, aborting.");
return res.status(401).send();
}
} catch (error) { console.log("--> HMAC ERROR", error); } };
I have used this function to validate webhooks requests. But both generateHash and hmac are always different in my case. Am I missing something?
There is no easy way to find the topic for a mandatory web-hook;
From documentations the list of these 3 mandantory webhooks is:
list 1:
however, all examples includes APP_UNINSTALLED but there is no example with the other three.
My question so far.. Is there a correlation between
list 1
and this list https://shopify.dev/api/admin-graphql/2021-10/enums/webhooksubscriptiontopic ?