Skip to content

Commit

Permalink
fix: Add Flagsmith signature header when testing webhook.
Browse files Browse the repository at this point in the history
Fixes #2786.
We are trying to create the same signature as the webhook
in the python code. This commit assumes that the python code
will use the same approach to create signature for long term.
  • Loading branch information
shubham-padia committed Apr 3, 2024
1 parent 2fc1e21 commit c731532
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 19 deletions.
2 changes: 2 additions & 0 deletions api/core/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
3 changes: 3 additions & 0 deletions api/edge_api/identities/edge_request_forwarder.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def _get_headers(request_method: str, headers: dict, payload: str = "") -> dict:
# ref: https://groups.google.com/g/django-developers/c/xjYVJN-RguA/m/G9krDqawchQJ
if request_method == "GET":
headers.pop("Content-Length", None)

# signPayload of frontend/web/components/TestWebHook on the frontend replicates this
# exact function, change the function there if this changes.
signature = sign_payload(payload, settings.EDGE_REQUEST_SIGNING_KEY)
headers[FLAGSMITH_SIGNATURE_HEADER] = signature
return headers
4 changes: 4 additions & 0 deletions api/webhooks/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ def _call_webhook(
headers = {"content-type": "application/json"}
json_data = json.dumps(data, sort_keys=True, cls=DjangoJSONEncoder)
if webhook.secret:
# signPayload of frontend/web/components/TestWebHook on the frontend replicates this
# exact function, change the function there if this changes.
signature = sign_payload(json_data, key=webhook.secret)
headers.update({FLAGSMITH_SIGNATURE_HEADER: signature})

Expand Down Expand Up @@ -199,6 +201,8 @@ def call_webhook_with_failure_mail_after_retries(
headers = {"content-type": "application/json"}
json_data = json.dumps(data, sort_keys=True, cls=DjangoJSONEncoder)
if webhook.secret:
# signPayload of frontend/web/components/TestWebHook on the frontend replicates this
# exact function, change the function there if this changes.
signature = sign_payload(json_data, key=webhook.secret)
headers.update({FLAGSMITH_SIGNATURE_HEADER: signature})

Expand Down
83 changes: 64 additions & 19 deletions frontend/web/components/TestWebhook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,79 @@ import Button from './base/forms/Button'
type TestWebhookType = {
webhook: string
json: string
secret: string
}

const TestWebhook: FC<TestWebhookType> = ({ 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<string> => {
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<TestWebhookType> = ({ json, secret, webhook }) => {
const [error, setError] = useState<string | null>(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 (
<div>
Expand Down
1 change: 1 addition & 0 deletions frontend/web/components/modals/CreateAuditWebhook.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const CreateAuditWebhook = class extends Component {
<TestWebhook
json={Constants.exampleAuditWebhook}
webhook={this.state.url}
secret={this.state.secret}
/>
{isEdit ? (
<Button
Expand Down
1 change: 1 addition & 0 deletions frontend/web/components/modals/CreateWebhook.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const CreateWebhook = class extends Component {
<TestWebhook
json={Constants.exampleWebhook}
webhook={this.state.url}
secret={this.state.secret}
/>
{isEdit ? (
<Button
Expand Down

0 comments on commit c731532

Please sign in to comment.