diff --git a/api/core/signing.py b/api/core/signing.py index f3495030f791..bb7851c45474 100644 --- a/api/core/signing.py +++ b/api/core/signing.py @@ -3,6 +3,8 @@ def sign_payload(payload: str, key: str): + # signPayload of frontend/web/components/TestWebHook on the frontend replicates this + # exact function, change the function there if this changes. return hmac.new( key=key.encode(), msg=payload.encode(), digestmod=hashlib.sha256 ).hexdigest() diff --git a/frontend/web/components/TestWebhook.tsx b/frontend/web/components/TestWebhook.tsx index d766b559fa72..176afeead339 100644 --- a/frontend/web/components/TestWebhook.tsx +++ b/frontend/web/components/TestWebhook.tsx @@ -8,34 +8,82 @@ import Button from './base/forms/Button' type TestWebhookType = { webhook: string json: string + secret: string } -const TestWebhook: FC = ({ json, webhook }) => { +// from https://stackoverflow.com/questions/24834812/space-in-between-json-stringify-output +const stringifyWithSpaces = (str: string) => { + const obj = JSON.parse(str) + let result = JSON.stringify(obj, null, 1) // stringify, with line-breaks and indents + result = result.replace(/^ +/gm, ' ') // remove all but the first space for each line + result = result.replace(/\n/g, '') // remove line-breaks + result = result.replace(/{ /g, '{').replace(/ }/g, '}') // remove spaces between object-braces and first/last props + result = result.replace(/\[ /g, '[').replace(/ \]/g, ']') // remove spaces between array-brackets and first/last items + return result +} + +const signPayload = async (body: string, secret: string): Promise => { + if(!secret) { + return '' + } + const enc = new TextEncoder() + + const key = await window.crypto.subtle.importKey( + 'raw', + enc.encode(secret), + { + hash: { name: 'SHA-256' }, + name: 'HMAC', + }, + false, + ['sign'], + ) + + const signature = await window.crypto.subtle.sign( + 'HMAC', + key, + enc.encode(stringifyWithSpaces(body)), // We do this bc the python output is single line with one space before each value + ) + const signatureUnsignedIntArray = new Uint8Array(signature) + return Array.prototype.map + .call(signatureUnsignedIntArray, (element) => + element.toString(16).padStart(2, '0'), + ) + .join('') +} + +const TestWebhook: FC = ({ json, secret, webhook }) => { const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [success, setSuccess] = useState(false) + const submit = () => { setError(null) setLoading(true) setSuccess(false) - data - .post(webhook, JSON.parse(json), null) - .then(() => { - setLoading(false) - setSuccess(true) - }) - .catch((e) => { - if (e.text) { - e.text().then((error: string) => { - setError(`The server returned an error: ${error}`) - }) - } else { - setError('There was an error posting to your webhook.') - } - }) - .finally(() => { - setLoading(false) - }) + signPayload(json, secret).then((sign) => { + const headers = { + 'X-Flagsmith-Signature': sign, + } + data + .post(webhook, JSON.parse(json), headers) + .then(() => { + setLoading(false) + setSuccess(true) + }) + .catch((e) => { + if (e.text) { + e.text().then((error: string) => { + setError(`The server returned an error: ${error}`) + }) + } else { + setError('There was an error posting to your webhook.') + } + }) + .finally(() => { + setLoading(false) + }) + }) } return (
diff --git a/frontend/web/components/modals/CreateAuditWebhook.js b/frontend/web/components/modals/CreateAuditWebhook.js index 2b07c9947bcc..6428fbb067e1 100644 --- a/frontend/web/components/modals/CreateAuditWebhook.js +++ b/frontend/web/components/modals/CreateAuditWebhook.js @@ -133,6 +133,7 @@ const CreateAuditWebhook = class extends Component { {isEdit ? (