finlight_client/
webhook.rs1use chrono::{DateTime, Utc};
2use hmac::{Hmac, KeyInit, Mac};
3use sha2::Sha256;
4
5use crate::error::WebhookVerificationError;
6use crate::models::Article;
7use crate::models::flex::parse_datetime;
8
9const SIGNATURE_PREFIX: &str = "sha256=";
10const REPLAY_TOLERANCE_SECS: i64 = 5 * 60;
11
12pub fn construct_webhook_event(
21 raw_body: &[u8],
22 signature: &str,
23 endpoint_secret: &str,
24 timestamp: Option<&str>,
25) -> Result<Article, WebhookVerificationError> {
26 let signature = signature
27 .strip_prefix(SIGNATURE_PREFIX)
28 .unwrap_or(signature);
29 let signature = hex::decode(signature)
30 .map_err(|_| WebhookVerificationError("Invalid webhook signature".into()))?;
31
32 let mut mac = Hmac::<Sha256>::new_from_slice(endpoint_secret.as_bytes())
33 .expect("HMAC accepts keys of any length");
34 if let Some(ts) = timestamp {
35 mac.update(ts.as_bytes());
36 mac.update(b".");
37 }
38 mac.update(raw_body);
39 mac.verify_slice(&signature)
40 .map_err(|_| WebhookVerificationError("Invalid webhook signature".into()))?;
41
42 if let Some(ts) = timestamp {
43 verify_timestamp(ts)?;
44 }
45
46 serde_json::from_slice(raw_body)
47 .map_err(|_| WebhookVerificationError("Invalid JSON payload".into()))
48}
49
50fn verify_timestamp(timestamp: &str) -> Result<(), WebhookVerificationError> {
51 let parsed: DateTime<Utc> = parse_datetime(timestamp)
52 .ok_or_else(|| WebhookVerificationError("Invalid timestamp format".into()))?;
53 let age = Utc::now().signed_duration_since(parsed).num_seconds();
54 if age.abs() > REPLAY_TOLERANCE_SECS {
55 return Err(WebhookVerificationError(
56 "Webhook timestamp outside allowed tolerance".into(),
57 ));
58 }
59 Ok(())
60}