rocket_webhook/webhooks/built_in/
shopify.rs

1use base64::{Engine, prelude::BASE64_STANDARD};
2use hmac::Hmac;
3use rocket::{Request, data::Outcome, http::Status, outcome::try_outcome};
4use sha2::Sha256;
5use zeroize::Zeroizing;
6
7use crate::{
8    WebhookError,
9    webhooks::{Webhook, interface::hmac::WebhookHmac},
10};
11
12/// # Shopify webhook
13/// Looks for base64 signature in `X-Shopify-Hmac-Sha256` header
14///
15/// [Shopify docs](https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-5-verify-the-webhook)
16pub struct ShopifyWebhook {
17    secret_key: Zeroizing<Vec<u8>>,
18}
19
20impl ShopifyWebhook {
21    /// Instantiate with the secret key
22    pub fn with_secret(secret_key: impl Into<Vec<u8>>) -> Self {
23        Self {
24            secret_key: Zeroizing::new(secret_key.into()),
25        }
26    }
27}
28
29impl Webhook for ShopifyWebhook {
30    async fn validate_body(
31        &self,
32        req: &Request<'_>,
33        body: impl rocket::tokio::io::AsyncRead + Unpin + Send + Sync,
34        time_bounds: (u32, u32),
35    ) -> Outcome<'_, Vec<u8>, WebhookError> {
36        self.validate_with_hmac(req, body, time_bounds).await
37    }
38}
39
40impl WebhookHmac for ShopifyWebhook {
41    type MAC = Hmac<Sha256>;
42
43    fn secret_key(&self) -> &[u8] {
44        &self.secret_key
45    }
46
47    fn expected_signatures(&self, req: &Request<'_>) -> Outcome<'_, Vec<Vec<u8>>, WebhookError> {
48        let sig_header = try_outcome!(self.get_header(req, "X-Shopify-Hmac-Sha256", None));
49        match BASE64_STANDARD.decode(sig_header) {
50            Ok(bytes) => Outcome::Success(vec![bytes]),
51            Err(_) => Outcome::Error((
52                Status::BadRequest,
53                WebhookError::InvalidHeader(format!(
54                    "X-Shopify-Hmac-Sha256 header was not valid base64: '{sig_header}'"
55                )),
56            )),
57        }
58    }
59}