Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add Flagsmith signature header when testing webhook. #3666

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
86 changes: 67 additions & 19 deletions frontend/web/components/TestWebhook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,82 @@ 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> => {
if(!secret) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, added this as it was rejecting attempting to sign with an empty secret. Instead it just resolves with '' in this case.

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<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
Loading