Skip to content

Instantly share code, notes, and snippets.

@waptik
Last active October 23, 2023 03:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save waptik/e2231da2c0f863d47b89b060db8fe49a to your computer and use it in GitHub Desktop.
Save waptik/e2231da2c0f863d47b89b060db8fe49a to your computer and use it in GitHub Desktop.
Immitating nodejs' cryptoHmac+Buffer.from in Deno by reproducing lemonsqueezy's webhook payload signing(https://github.com/amosbastian/template/blob/bdc77731d4f0dfef0bd2529205c8dd5b733d5b4a/apps/web/app/api/lemonsqueezy/route.ts#L76)
// @see https://github.com/denoland/deno/blob/main/cli/tests/unit_node/crypto/crypto_hash_test.ts#L17
export * as crypto from 'node:crypto';
// logs in terminal
verifySignature {
  digest: "72bd23aa53d2534f4c3c7dc2ed14bbfa5784bc51b4d5e357d6db81401687e253",
  signatureHeader: "05f2c5e80f9a44cf1aa554aa18f82ba4202d9ead7fa4cc4b954b126525fcd9b8",
}
Signing secret is invalid
❌ LemonSqueezy Webhook Error message: Signing secret is invalid
import { Router } from 'oak';
import { createHmac, timingSafeEqual } from 'crypto.ts';
enum LemonSqueezyWebhooks {
SubscriptionCreated = 'subscription_created',
SubscriptionUpdated = 'subscription_updated',
SubscriptionPaymentSuccess = 'subscription_payment_success',
OrderCreated = 'order_created',
}
const lemonsqueezyRouter = new Router();
lemonsqueezyRouter.post('/lemonsqueezy', async (ctx) => {
const signatureHeader = ctx.request.headers.get('x-signature');
const eventName = ctx.request.headers.get('x-event-name');
const body = await ctx.request.body({ type: 'text' }).value;
console.log(`lemonsqueezy webhook body.json`, body);
try {
if (!signatureHeader) {
console.error(`Signature header not found`);
throw new Error('Signature header not found');
}
const webhookSecret = Deno.env.get('LEMONSQUEEZY_WEBHOOK_SECRET') ?? '';
const encoder = new TextEncoder();
const hmac = createHmac(
'sha256',
webhookSecret,
);
const digestString = hmac.update(payload).digest('hex');
const digest = encoder.encode(digestString);
const signature = encoder.encode(
signatureHeader,
);
console.log('LemonSqueezy.verifySignature', {
digest,
signatureHeader,
webhookSecret,
digestString
});
const result = timingSafeEqual(digest, signature);
console.log(`verifySignature`, result);
console.log('verifySignature', {
digest,
signatureHeader,
});
const isSigningSecretValid = timingSafeEqual(digest, signature);
if (!isSigningSecretValid) {
console.error(`Signing secret is invalid`);
throw new Error('Signing secret is invalid');
}
} catch (err) {
console.log(`❌ LemonSqueezy Webhook Error message: ${err.message}`);
ctx.response.status = 401;
ctx.response.body = `Unauthorized: ${err.message}`;
return;
}
console.info(
`[Lemon Squeezy] Received Webhook`,
{
type: eventName,
},
);
switch (body.meta.event_name) {
case LemonSqueezyWebhooks.OrderCreated: {
// data.data is an Order
console.log({ body });
break;
}
default:
break;
}
ctx.response.body = 'success';
ctx.response.status = 200;
});
export default lemonsqueezyRouter;
@waptik
Copy link
Author

waptik commented Oct 21, 2023

It fails with lemonsqueezy payload but works with another provider

// nodejs version
 const hash = crypto.createHmac('sha512', secret).update(JSON.stringify(req.body)).digest('hex');

    if (hash == req.headers['x-pyst-signature']) {

    // Retrieve the request's body

    const event = req.body;

    // Do something with event  

    }

// deno version
const signatureHeader = ctx.request.headers.get('x-pyst-signature');
	const hash = await hmacSha(
			apiKey,
			JSON.stringify(body),
			true,
			'512',
		);

if(hash === signatureHeader){
    // Do something with event  
}

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