diff --git a/docs/integrations/integration-platform/public-integration.mdx b/docs/integrations/integration-platform/public-integration.mdx index 7ef265e0f221c9..6692c368cb2f07 100644 --- a/docs/integrations/integration-platform/public-integration.mdx +++ b/docs/integrations/integration-platform/public-integration.mdx @@ -168,12 +168,13 @@ def refresh_token(install_id): ## Verifying Installations -Typically, if you have the redirect URL configured, there is work happening on your end to "finalize" the installation. If this is the case, we recommend enabling the "Verify Install" option for your integration. Once enabled, you'll need to send a request marking the installation as officially "installed": +Typically, if you have the redirect URL configured, there is work happening on your end to "finalize" the installation. If this is the case, we recommend enabling the "Verify Install" option for your integration. Once enabled, you'll need to send a request marking the installation as officially "installed". Authenticate the request with the token you received for this installation during the [OAuth process](#oauth-process): ```python requests.put( u'https://sentry.io/api/0/sentry-app-installations/{}/'.format(install_id), json={'status': 'installed'}, + headers={'Authorization': f'Bearer {token}'}, ) ``` diff --git a/docs/integrations/integration-platform/ui-components/index.mdx b/docs/integrations/integration-platform/ui-components/index.mdx index 0e1718b3e16fda..0860526e8ce30b 100644 --- a/docs/integrations/integration-platform/ui-components/index.mdx +++ b/docs/integrations/integration-platform/ui-components/index.mdx @@ -36,6 +36,15 @@ The UI components are specified in the schema section of the integration details The `uri` keys in your schema elements describe the URI path that will receive a request from that UI component. Sentry will append the path in the `uri` field onto the host of your integration's _webhook URL_. For example, if the webhook URL is `https://example.com/webhook/`, and your schema's `uri` attribute is `/alerts/`, then the integration will receive requests to `https://example.com/alerts/`. +## Verifying Requests + +Sentry signs requests from UI components with your _Client Secret_, the same way it signs [webhooks](/integrations/integration-platform/webhooks/#sentry-hook-signature). The only difference is the header name: UI component requests carry the signature in the `Sentry-App-Signature` header, not `Sentry-Hook-Signature`. + +Verify the signature against the raw request body, using the same code as for webhooks. Keep these details in mind: + +- [Select](/integrations/integration-platform/ui-components/formfield/#select) option requests are `GET` requests with no body, so Sentry computes the signature over an empty string. These requests still include a `Content-Type: application/json` header, so make sure your framework doesn't reject or rewrite the empty body before you verify it. +- The signature covers only the body. The query parameters of select option requests, such as `installationId`, aren't signed. + ## Error Handling Component rendering either 100% works or shows nothing. To protect the integration from looking chaotic due to errors we have no control over, if any part of the third-party component rendering fails, nothing will render. diff --git a/docs/integrations/integration-platform/webhooks.mdx b/docs/integrations/integration-platform/webhooks.mdx index c5735672722cd0..8ecc48ca70e6f8 100644 --- a/docs/integrations/integration-platform/webhooks.mdx +++ b/docs/integrations/integration-platform/webhooks.mdx @@ -55,40 +55,53 @@ This header represents a cryptographic hash generated by your _Client Secret_. I **Verifying the Signature** -The below code snippet lets you validate the signature with the event payload. +Sentry computes the signature as an HMAC-SHA256 hex digest of the exact request body bytes, using your _Client Secret_ as the key. Compute the digest over the raw body as received. Don't parse the JSON and serialize it again, because the result won't always match the bytes Sentry signed. ```javascript const crypto = require("crypto"); -function verifySignature(request, secret = "") { - const hmac = crypto.createHmac("sha256", secret); - hmac.update(JSON.stringify(request.body), "utf8"); - const digest = hmac.digest("hex"); - return digest === request.headers["sentry-hook-signature"]; +// rawBody must be the unparsed request body, for example from +// express.raw({ type: "application/json" }) +function verifySignature(rawBody, signature, secret) { + if (!signature) { + return false; + } + const digest = Buffer.from( + crypto.createHmac("sha256", secret).update(rawBody).digest("hex") + ); + const expected = Buffer.from(signature); + return ( + digest.length === expected.length && + crypto.timingSafeEqual(digest, expected) + ); } + +verifySignature(request.body, request.headers["sentry-hook-signature"], secret); ``` ```python import hashlib import hmac -import json -expected_digest = request.headers.get('sentry-hook-signature') # returns None if header is missing -body = json.dumps(request.body) -digest = hmac.new( - key=client_secret.encode('utf-8'), - msg=body, - digestmod=hashlib.sha256, -).hexdigest() +def verify_signature(raw_body: bytes, signature: str | None, client_secret: str) -> bool: + if not signature: # The signature is missing + return False + digest = hmac.new( + key=client_secret.encode("utf-8"), + msg=raw_body, + digestmod=hashlib.sha256, + ).hexdigest() + return hmac.compare_digest(digest, signature) -if not expected_digest: # The signature is missing - raise UnauthorizedError -if not hmac.compare_digest(digest, expected_digest): +# For example, in Flask: +if not verify_signature(request.get_data(), request.headers.get("sentry-hook-signature"), client_secret): raise UnauthorizedError ``` +Requests from [UI components](/integrations/integration-platform/ui-components/#verifying-requests) are signed the same way, but use the `Sentry-App-Signature` header instead. + ## Request Structure All webhook requests have some common elements.