admin管理员组文章数量:1302533
I have figured out the problem thanks to the extensive answer from @user9014097. This section in particular describes my mistake/oversight:
The formatting of the message plays a role in determining the MAC. Every difference, e.g. a line break, a blank etc. changes the signature and results in a failed verification. Check if you might have changed the message or its format a little bit.
After stringifying the request body it works like a charm!
With cloudflare workers you can get the original body in plain text like this: const payload = await event.request.text();
Original post:
I am trying to manually verify the signature for Stripe webhooks. I am not working in node.js so the stripe-node package is not an option for me unfortunately. I have followed the "Verifying signatures manually" steps on . So far I've produced the following:
- body: event.request.body (from cloudflare workers' fetch event)
- header: event.request.headers.get('Stripe-Signature')
const hexStringToUint8Array = hexString => {
const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
for (let i = 0; i < bytes.length; i++)
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
return bytes;
};
export const verifySignature = async (body, header, tolerance = 300) => {
header = header.split(',').reduce((accum, x) => {
const [k, v] = x.split('=');
return { ...accum, [k]: v };
}, {});
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(STRIPE_WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const verified = await crypto.subtle.verify(
"HMAC",
key,
hexStringToUint8Array(header.v1),
encoder.encode(`${header.t}.${body}`)
);
const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
return verified && !(tolerance && elapsed > tolerance)
};
However the verify function always returns false. Can anyone spot the issue here?
Thank you, Jacco
Edit: Here follows test data. Thanks @user9014097 for the request:
The body and header are supposed to be used as the parameters for verifySignature.
body
{
"id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
"object": "event",
"api_version": "2020-08-27",
"created": 1625669316,
"data": {
"object": {
"id": "prod_Jkre4DaakpOaCt",
"object": "product",
"active": true,
"attributes": [
],
"created": 1624892313,
"description": null,
"images": [
";
],
"livemode": false,
"metadata": {
"brand": "DOM",
"series": "1D",
"key_codes_start": "1",
"key_codes_end": "114"
},
"name": "DOM 1D serie 1-114",
"package_dimensions": null,
"shippable": null,
"statement_descriptor": null,
"type": "service",
"unit_label": "sleutel",
"updated": 1625669316,
"url": null
},
"previous_attributes": {
"description": "test",
"updated": 1625665952
}
},
"livemode": false,
"pending_webhooks": 1,
"request": {
"id": "req_SxhB93mIUlcaKW",
"idempotency_key": null
},
"type": "product.updated"
}
header
t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2
The STRIPE_WEBHOOK_SECRET variable inside the verifySignature function is used to import/create the key. Which is then used to verify the payload/body. In order to test it you can swap out the variable name for the secret string below.
STRIPE_WEBHOOK_SECRET
whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j
I have figured out the problem thanks to the extensive answer from @user9014097. This section in particular describes my mistake/oversight:
The formatting of the message plays a role in determining the MAC. Every difference, e.g. a line break, a blank etc. changes the signature and results in a failed verification. Check if you might have changed the message or its format a little bit.
After stringifying the request body it works like a charm!
With cloudflare workers you can get the original body in plain text like this: const payload = await event.request.text();
Original post:
I am trying to manually verify the signature for Stripe webhooks. I am not working in node.js so the stripe-node package is not an option for me unfortunately. I have followed the "Verifying signatures manually" steps on https://stripe./docs/webhooks/signatures#verify-manually. So far I've produced the following:
- body: event.request.body (from cloudflare workers' fetch event)
- header: event.request.headers.get('Stripe-Signature')
const hexStringToUint8Array = hexString => {
const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
for (let i = 0; i < bytes.length; i++)
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
return bytes;
};
export const verifySignature = async (body, header, tolerance = 300) => {
header = header.split(',').reduce((accum, x) => {
const [k, v] = x.split('=');
return { ...accum, [k]: v };
}, {});
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(STRIPE_WEBHOOK_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const verified = await crypto.subtle.verify(
"HMAC",
key,
hexStringToUint8Array(header.v1),
encoder.encode(`${header.t}.${body}`)
);
const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
return verified && !(tolerance && elapsed > tolerance)
};
However the verify function always returns false. Can anyone spot the issue here?
Thank you, Jacco
Edit: Here follows test data. Thanks @user9014097 for the request:
The body and header are supposed to be used as the parameters for verifySignature.
body
{
"id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
"object": "event",
"api_version": "2020-08-27",
"created": 1625669316,
"data": {
"object": {
"id": "prod_Jkre4DaakpOaCt",
"object": "product",
"active": true,
"attributes": [
],
"created": 1624892313,
"description": null,
"images": [
"https://files.stripe./links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE"
],
"livemode": false,
"metadata": {
"brand": "DOM",
"series": "1D",
"key_codes_start": "1",
"key_codes_end": "114"
},
"name": "DOM 1D serie 1-114",
"package_dimensions": null,
"shippable": null,
"statement_descriptor": null,
"type": "service",
"unit_label": "sleutel",
"updated": 1625669316,
"url": null
},
"previous_attributes": {
"description": "test",
"updated": 1625665952
}
},
"livemode": false,
"pending_webhooks": 1,
"request": {
"id": "req_SxhB93mIUlcaKW",
"idempotency_key": null
},
"type": "product.updated"
}
header
t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2
The STRIPE_WEBHOOK_SECRET variable inside the verifySignature function is used to import/create the key. Which is then used to verify the payload/body. In order to test it you can swap out the variable name for the secret string below.
STRIPE_WEBHOOK_SECRET
whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j
Share
Improve this question
edited Jul 8, 2021 at 9:44
J. van Mourik
asked Jul 7, 2021 at 15:15
J. van MourikJ. van Mourik
781 silver badge7 bronze badges
5
- Can you provide test data? – Topaco Commented Jul 7, 2021 at 19:39
- When you say you're not working in Node what does that mean? It looks like you're using Node code, libraries, etc. As far as figuring our your issue, how are you getting the body of the request? It must be pletely unaltered or the signature check will fail. – Justin Michael Commented Jul 7, 2021 at 20:35
- @JustinMichael I'm building on Cloudflare's workers, it provides their own set of tools to do some of the basic things that node does. FetchEvent for example provides the event object, this event object contains the request object (and its body). The body is passed unaltered to the verifySignature function as the first parameter. – J. van Mourik Commented Jul 7, 2021 at 23:27
- @user9014097 I've made an edit to the original post to include test data. Thanks. – J. van Mourik Commented Jul 7, 2021 at 23:47
-
Thanks a lot for documenting this! Tiny detail: Unless I'm mistaken, the
hexStringToUint8Array
function can be replaced byUint8Array.from(Buffer.from(signature.v1, "hex"))
. Or am I missing any special cases where this fails? – Thomas Jaggi Commented Feb 14, 2024 at 14:19
2 Answers
Reset to default 6On my machine, the actual verification of the signature is successful!
However, your verification also takes into account the timestamp. If the verification time differs from this timestamp by more than a given tolerance value (default 300s), the verification fails. It is this last condition that causes the verification to fail.
If the tolerance is enough or the message timestamp is within the tolerance, the verification succeeds:
(async () => {
const hexStringToUint8Array = hexString => {
const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
for (let i = 0; i < bytes.length; i++)
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
return bytes;
};
const verifySignature = async (body, header, tolerance = 300) => {
header = header.split(',').reduce((accum, x) => {
const [k, v] = x.split('=');
return { ...accum, [k]: v };
}, {});
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode('whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j'),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const verified = await crypto.subtle.verify(
"HMAC",
key,
hexStringToUint8Array(header.v1),
encoder.encode(`${header.t}.${body}`)
);
const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
return verified && !(tolerance && elapsed > tolerance)
};
var body = `{
"id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
"object": "event",
"api_version": "2020-08-27",
"created": 1625669316,
"data": {
"object": {
"id": "prod_Jkre4DaakpOaCt",
"object": "product",
"active": true,
"attributes": [
],
"created": 1624892313,
"description": null,
"images": [
"https://files.stripe./links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE"
],
"livemode": false,
"metadata": {
"brand": "DOM",
"series": "1D",
"key_codes_start": "1",
"key_codes_end": "114"
},
"name": "DOM 1D serie 1-114",
"package_dimensions": null,
"shippable": null,
"statement_descriptor": null,
"type": "service",
"unit_label": "sleutel",
"updated": 1625669316,
"url": null
},
"previous_attributes": {
"description": "test",
"updated": 1625665952
}
},
"livemode": false,
"pending_webhooks": 1,
"request": {
"id": "req_SxhB93mIUlcaKW",
"idempotency_key": null
},
"type": "product.updated"
}`
var header = `t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2`;
const elapsed = Math.floor(Date.now() / 1000) - Number(1625700981);
console.log("Elapsed time in s:", elapsed)
console.log("Verification without considering tolerance:", await verifySignature(body, header, null));
console.log("Verification with enough tolerance: ", await verifySignature(body, header, elapsed));
console.log("Verification with default tolerance: ", await verifySignature(body, header)); // default: tolerance = 300
})();
A failure of the verification in your environment could have e.g. the following reasons:
- A too small tolerance (the default value of 300s is meanwhile (!) too small for the timestamp of the posted nessage).
- The formatting of the message plays a role in determining the MAC. Every difference, e.g. a line break, a blank etc. changes the signature and results in a failed verification. Check if you might have changed the message or its format a little bit.
While you can't use the entire Stripe Node library in a Cloudflare Worker you can use the webhook signature piece.
本文标签: javascriptHow to verify Stripe39s webhook signatureStack Overflow
版权声明:本文标题:javascript - How to verify Stripe's webhook signature? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741695182a2392963.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论