1use std::str::FromStr;
2
3use chrono::Utc;
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use stripe_shared::event::EventType;
7use stripe_shared::ApiVersion;
8
9use crate::{EventObject, WebhookError};
10
11#[derive(Clone, Debug)]
12#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
13#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
14pub struct Event {
15 pub account: Option<String>,
17 pub api_version: Option<ApiVersion>,
21 pub created: stripe_types::Timestamp,
25 pub data: EventData,
26 pub id: stripe_shared::event::EventId,
28 pub livemode: bool,
30 pub pending_webhooks: i64,
32 pub request: Option<stripe_shared::NotificationEventRequest>,
34 pub type_: EventType,
36}
37
38#[derive(Clone, Debug)]
39#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
40#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
41pub struct EventData {
42 pub object: EventObject,
46 #[cfg_attr(
51 any(feature = "deserialize", feature = "serialize"),
52 serde(with = "stripe_types::with_serde_json_opt")
53 )]
54 pub previous_attributes: Option<miniserde::json::Value>,
55}
56
57#[derive(Debug)]
58pub struct Webhook {
59 current_timestamp: i64,
60}
61
62impl Webhook {
63 pub fn insecure(payload: &str) -> Result<Event, WebhookError> {
72 Self { current_timestamp: 0 }.parse_payload(payload)
73 }
74
75 pub fn construct_event(payload: &str, sig: &str, secret: &str) -> Result<Event, WebhookError> {
85 Self { current_timestamp: Utc::now().timestamp() }.do_construct_event(payload, sig, secret)
86 }
87
88 pub fn construct_event_with_timestamp(
102 payload: &str,
103 sig: &str,
104 secret: &str,
105 timestamp: i64,
106 ) -> Result<Event, WebhookError> {
107 Self { current_timestamp: timestamp }.do_construct_event(payload, sig, secret)
108 }
109
110 fn do_construct_event(
111 self,
112 payload: &str,
113 sig: &str,
114 secret: &str,
115 ) -> Result<Event, WebhookError> {
116 let signature = Signature::parse(sig)?;
118 let signed_payload = format!("{}.{}", signature.t, payload);
119
120 let mut mac =
123 Hmac::<Sha256>::new_from_slice(secret.as_bytes()).map_err(|_| WebhookError::BadKey)?;
124 mac.update(signed_payload.as_bytes());
125
126 let sig = hex::decode(signature.v1).map_err(|_| WebhookError::BadSignature)?;
127 mac.verify_slice(sig.as_slice()).map_err(|_| WebhookError::BadSignature)?;
128
129 if (self.current_timestamp - signature.t).abs() > 300 {
131 return Err(WebhookError::BadTimestamp(signature.t));
132 }
133
134 self.parse_payload(payload)
135 }
136
137 fn parse_payload(self, payload: &str) -> Result<Event, WebhookError> {
138 let base_evt: stripe_shared::Event = miniserde::json::from_str(payload)
139 .map_err(|_| WebhookError::BadParse("could not deserialize webhook event".into()))?;
140
141 let event_obj =
142 EventObject::from_raw_data(base_evt.type_.as_str(), base_evt.data.object)
143 .ok_or_else(|| WebhookError::BadParse("could not parse event object".into()))?;
144
145 Ok(Event {
146 account: base_evt.account,
147 api_version: base_evt
148 .api_version
149 .map(|s| ApiVersion::from_str(&s).unwrap_or(ApiVersion::Unknown(s))),
150 created: base_evt.created,
151 data: EventData {
152 object: event_obj,
153 previous_attributes: base_evt.data.previous_attributes,
154 },
155 id: base_evt.id,
156 livemode: base_evt.livemode,
157 pending_webhooks: base_evt.pending_webhooks,
158 request: base_evt.request,
159 type_: base_evt.type_,
160 })
161 }
162}
163
164#[derive(Debug)]
165struct Signature<'r> {
166 t: i64,
167 v1: &'r str,
168}
169
170impl<'r> Signature<'r> {
171 fn parse(raw: &'r str) -> Result<Signature<'r>, WebhookError> {
172 let mut t: Option<i64> = None;
173 let mut v1: Option<&'r str> = None;
174 for pair in raw.split(',') {
175 let (key, val) = pair.split_once('=').ok_or(WebhookError::BadSignature)?;
176 match key {
177 "t" => {
178 t = Some(val.parse().map_err(WebhookError::BadHeader)?);
179 }
180 "v1" => {
181 v1 = Some(val);
182 }
183 _ => {}
184 }
185 }
186 Ok(Signature {
187 t: t.ok_or(WebhookError::BadSignature)?,
188 v1: v1.ok_or(WebhookError::BadSignature)?,
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use serde_json::{json, Value};
196
197 use super::*;
198 use crate::{AccountExternalAccountCreated, EventType};
199
200 const WEBHOOK_SECRET: &str = "secret";
201
202 #[test]
203 fn test_signature_parse() {
204 let raw_signature =
205 "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd";
206 let signature = Signature::parse(raw_signature).unwrap();
207 assert_eq!(signature.t, 1492774577);
208 assert_eq!(
209 signature.v1,
210 "5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
211 );
212
213 let raw_signature_with_test_mode = "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39";
214 let signature = Signature::parse(raw_signature_with_test_mode).unwrap();
215 assert_eq!(signature.t, 1492774577);
216 assert_eq!(
217 signature.v1,
218 "5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
219 );
220 }
221
222 fn get_mock_stripe_sig(msg: &str, timestamp: i64) -> String {
223 let signed_payload = format!("{timestamp}.{msg}");
224
225 let mut mac = Hmac::<Sha256>::new_from_slice(WEBHOOK_SECRET.as_bytes()).unwrap();
226
227 mac.update(signed_payload.as_bytes());
228 let result = mac.finalize().into_bytes();
229 let v1 = hex::encode(&result[..]);
230 format!("t={timestamp},v1={v1}")
231 }
232
233 fn mock_webhook_event(event_type: &EventType, data: Value) -> Value {
234 json!({
235 "id": "evt_123",
236 "object": "event",
237 "account": "acct_123",
238 "api_version": "2017-05-25",
239 "created": 1533204620,
240 "livemode": false,
241 "pending_webhooks": 1,
242 "request": {
243 "id": "req_123",
244 "idempotency_key": "idempotency-key-123"
245 },
246 "data": {
247 "object": data,
248 },
249 "type": event_type.to_string()
250 })
251 }
252
253 #[track_caller]
254 fn parse_mock_webhook_event(event_type: EventType, data: Value) -> EventObject {
255 let now = Utc::now().timestamp();
256 let payload = mock_webhook_event(&event_type, data).to_string();
257 let sig = get_mock_stripe_sig(&payload, now);
258
259 let webhook = Webhook { current_timestamp: now };
260 let parsed = webhook.do_construct_event(&payload, &sig, WEBHOOK_SECRET).unwrap();
261 assert_eq!(parsed.type_, event_type);
262 parsed.data.object
263 }
264
265 #[test]
266 fn test_webhook_construct_event() {
267 let object = json!({
268 "id": "ii_123",
269 "object": "invoiceitem",
270 "amount": 1000,
271 "currency": "usd",
272 "customer": "cus_123",
273 "date": 1533204620,
274 "description": "Test Invoice Item",
275 "discountable": false,
276 "invoice": "in_123",
277 "livemode": false,
278 "metadata": {},
279 "period": {
280 "start": 1533204620,
281 "end": 1533204620
282 },
283 "proration": false,
284 "quantity": 3
285 });
286 let payload = mock_webhook_event(&EventType::InvoiceitemCreated, object);
287 let event_timestamp = 1533204620;
288 let signature = format!("t={event_timestamp},v1=5a81ebe328da1df19581cbc6c7377920947ffd30b56eebcc7ba9a6938a090965,v0=63f3a72374a733066c4be69ed7f8e5ac85c22c9f0a6a612ab9a025a9e4ee7eef");
289
290 let webhook = Webhook { current_timestamp: event_timestamp };
291
292 let event = webhook
293 .do_construct_event(&payload.to_string(), &signature, WEBHOOK_SECRET)
294 .expect("Failed to construct event");
295
296 assert_eq!(event.type_, EventType::InvoiceitemCreated);
297 assert_eq!(event.id.as_str(), "evt_123",);
298 assert_eq!(event.account, "acct_123".parse().ok());
299 assert_eq!(event.created, 1533204620);
300
301 let EventObject::InvoiceitemCreated(invoice) = event.data.object else {
302 panic!("expected invoice item created");
303 };
304 assert_eq!(invoice.id.as_str(), "ii_123");
305 assert_eq!(invoice.quantity, 3);
306 }
307
308 #[cfg(feature = "async-stripe-billing")]
309 #[test]
310 fn test_billing_portal_session() {
312 let object = json!({
313 "configuration": "bpc_123",
314 "created": 1533204620,
315 "customer": "cus_123",
316 "id": "bps_123",
317 "livemode": false,
318 "url": "http://localhost:3000"
319 });
320 let result = parse_mock_webhook_event(EventType::BillingPortalSessionCreated, object);
321 let EventObject::BillingPortalSessionCreated(session) = result else {
322 panic!("expected billing portal session");
323 };
324 assert_eq!(session.url, "http://localhost:3000");
325 assert_eq!(session.id.as_str(), "bps_123");
326 assert_eq!(session.configuration.id().as_str(), "bpc_123");
327 }
328
329 #[test]
330 fn deserialize_polymorphic() {
331 let object = json!({
332 "object": "bank_account",
333 "country": "us",
334 "currency": "gbp",
335 "id": "ba_123",
336 "last4": "1234",
337 "status": "status",
338 });
339 let result = parse_mock_webhook_event(EventType::AccountExternalAccountCreated, object);
340 let EventObject::AccountExternalAccountCreated(AccountExternalAccountCreated::BankAccount(
341 bank_account,
342 )) = result
343 else {
344 panic!("unexpected type parsed");
345 };
346 assert_eq!(bank_account.id.as_str(), "ba_123");
347 assert_eq!(bank_account.last4, "1234");
348 }
349}