1use std::str::FromStr;
2
3use chrono::Utc;
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use stripe_shared::ApiVersion;
7use stripe_shared::event::EventType;
8
9use crate::{EventObject, WebhookError};
10
11#[derive(Clone, Debug)]
12#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
13pub struct Event {
14 #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
16 pub account: Option<String>,
17 #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
21 pub api_version: Option<ApiVersion>,
22 pub created: stripe_types::Timestamp,
26 pub data: EventData,
27 pub id: stripe_shared::event::EventId,
29 pub livemode: bool,
31 #[cfg_attr(feature = "serialize", serde(rename = "object"))]
33 pub object: EventObjectType,
34 pub pending_webhooks: i64,
36 #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
38 pub request: Option<stripe_shared::NotificationEventRequest>,
39 #[cfg_attr(feature = "serialize", serde(rename = "type"))]
41 pub type_: EventType,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
47#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
48pub enum EventObjectType {
49 #[cfg_attr(any(feature = "serialize", feature = "deserialize"), serde(rename = "event"))]
50 Event,
51}
52
53#[derive(Clone, Debug)]
54#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
55#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
56pub struct EventData {
57 pub object: EventObject,
61 #[cfg_attr(
66 any(feature = "deserialize", feature = "serialize"),
67 serde(with = "stripe_types::with_serde_json_opt")
68 )]
69 #[cfg_attr(feature = "serialize", serde(skip_serializing_if = "Option::is_none"))]
70 pub previous_attributes: Option<miniserde::json::Value>,
71}
72
73#[cfg(feature = "deserialize")]
76impl<'de> serde::Deserialize<'de> for Event {
77 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78 where
79 D: serde::Deserializer<'de>,
80 {
81 use serde::de::Error;
82
83 #[derive(serde::Deserialize)]
85 struct EventProxy {
86 pub account: Option<String>,
87 pub api_version: Option<ApiVersion>,
88 pub created: stripe_types::Timestamp,
89 pub id: stripe_shared::event::EventId,
90 pub livemode: bool,
91 #[serde(rename = "object")]
92 #[allow(dead_code)]
93 pub object_type: String, pub pending_webhooks: i64,
95 pub request: Option<stripe_shared::NotificationEventRequest>,
96 #[serde(rename = "type")]
97 pub type_: EventType,
98 pub data: serde_json::Value,
99 }
100
101 let proxy = EventProxy::deserialize(deserializer)?;
103
104 let object_value =
106 proxy.data.get("object").ok_or_else(|| Error::missing_field("data.object"))?.clone();
107
108 let object =
110 EventObject::from_json_value(proxy.type_.as_str(), object_value).map_err(|e| {
111 if e.contains(':') {
114 Error::custom(format!("data.object.{e}"))
115 } else {
116 Error::custom(format!("data.object: {e}"))
118 }
119 })?;
120
121 let previous_attributes =
123 if let Some(prev_attrs) = proxy.data.get("previous_attributes") {
124 let prev_attrs_str = serde_json::to_string(prev_attrs).map_err(|e| {
125 Error::custom(format!("Failed to serialize previous_attributes: {e}"))
126 })?;
127 Some(miniserde::json::from_str(&prev_attrs_str).map_err(|e| {
128 Error::custom(format!("Failed to parse previous_attributes: {e}"))
129 })?)
130 } else {
131 None
132 };
133
134 Ok(Event {
135 account: proxy.account,
136 api_version: proxy.api_version,
137 created: proxy.created,
138 data: EventData { object, previous_attributes },
139 id: proxy.id,
140 livemode: proxy.livemode,
141 object: EventObjectType::Event,
142 pending_webhooks: proxy.pending_webhooks,
143 request: proxy.request,
144 type_: proxy.type_,
145 })
146 }
147}
148
149#[derive(Debug)]
150pub struct Webhook {
151 current_timestamp: i64,
152}
153
154impl Webhook {
155 pub fn generate_test_header(payload: &str, secret: &str, timestamp: Option<i64>) -> String {
205 let timestamp = timestamp.unwrap_or_else(|| Utc::now().timestamp());
206 let signed_payload = format!("{timestamp}.{payload}");
207
208 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
209 .expect("HMAC can take key of any size");
210 mac.update(signed_payload.as_bytes());
211 let result = mac.finalize().into_bytes();
212 let v1 = hex::encode(&result[..]);
213
214 format!("t={timestamp},v1={v1}")
215 }
216
217 pub fn insecure(payload: &str) -> Result<Event, WebhookError> {
226 if !cfg!(debug_assertions) {
227 tracing::warn!(
228 "Webhook::insecure() bypasses signature verification and should only be used for local testing. \
229 Use Webhook::construct_event() for production code."
230 );
231 }
232 Self { current_timestamp: 0 }.parse_payload(payload)
233 }
234
235 pub fn construct_event(payload: &str, sig: &str, secret: &str) -> Result<Event, WebhookError> {
245 Self { current_timestamp: Utc::now().timestamp() }.do_construct_event(payload, sig, secret)
246 }
247
248 pub fn construct_event_with_timestamp(
262 payload: &str,
263 sig: &str,
264 secret: &str,
265 timestamp: i64,
266 ) -> Result<Event, WebhookError> {
267 Self { current_timestamp: timestamp }.do_construct_event(payload, sig, secret)
268 }
269
270 fn do_construct_event(
271 self,
272 payload: &str,
273 sig: &str,
274 secret: &str,
275 ) -> Result<Event, WebhookError> {
276 let signature = Signature::parse(sig)?;
278 let signed_payload = format!("{}.{}", signature.t, payload);
279
280 let mut mac =
283 Hmac::<Sha256>::new_from_slice(secret.as_bytes()).map_err(|_| WebhookError::BadKey)?;
284 mac.update(signed_payload.as_bytes());
285
286 let sig = hex::decode(signature.v1).map_err(|_| WebhookError::BadSignature)?;
287 mac.verify_slice(sig.as_slice()).map_err(|_| WebhookError::BadSignature)?;
288
289 if (self.current_timestamp - signature.t).abs() > 300 {
291 return Err(WebhookError::BadTimestamp(signature.t));
292 }
293
294 self.parse_payload(payload)
295 }
296
297 #[tracing::instrument]
298 fn parse_payload(self, payload: &str) -> Result<Event, WebhookError> {
299 let base_evt: stripe_shared::Event = miniserde::json::from_str(payload)
300 .map_err(|_| WebhookError::BadParse("could not deserialize webhook event".into()))?;
301
302 let event_obj =
303 EventObject::from_raw_data(base_evt.type_.as_str(), base_evt.data.object)
304 .ok_or_else(|| WebhookError::BadParse("could not parse event object".into()))?;
305
306 let api_version = base_evt.api_version.as_ref().and_then(|s| ApiVersion::from_str(s).ok());
308
309 if let Some(event_version) = &api_version {
310 if event_version != &stripe_shared::version::VERSION {
311 tracing::warn!(
312 event_version=?event_version,
313 sdk_version=?stripe_shared::version::VERSION,
314 "API version mismatch: SDK compiled with {:?}, but event received with {:?}",
315 stripe_shared::version::VERSION,
316 event_version
317 );
318 }
319 }
320
321 Ok(Event {
322 account: base_evt.account,
323 api_version: base_evt
324 .api_version
325 .map(|s| ApiVersion::from_str(&s).expect("infallible")),
326 created: base_evt.created,
327 data: EventData {
328 object: event_obj,
329 previous_attributes: base_evt.data.previous_attributes,
330 },
331 id: base_evt.id,
332 livemode: base_evt.livemode,
333 object: EventObjectType::Event,
334 pending_webhooks: base_evt.pending_webhooks,
335 request: base_evt.request,
336 type_: base_evt.type_,
337 })
338 }
339}
340
341#[derive(Debug)]
342struct Signature<'r> {
343 t: i64,
344 v1: &'r str,
345}
346
347impl<'r> Signature<'r> {
348 fn parse(raw: &'r str) -> Result<Signature<'r>, WebhookError> {
349 let mut t: Option<i64> = None;
350 let mut v1: Option<&'r str> = None;
351 for pair in raw.split(',') {
352 let (key, val) = pair.split_once('=').ok_or(WebhookError::BadSignature)?;
353 match key {
354 "t" => {
355 t = Some(val.parse().map_err(WebhookError::BadHeader)?);
356 }
357 "v1" => {
358 v1 = Some(val);
359 }
360 _ => {}
361 }
362 }
363 Ok(Signature {
364 t: t.ok_or(WebhookError::BadSignature)?,
365 v1: v1.ok_or(WebhookError::BadSignature)?,
366 })
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use serde_json::{Value, json};
373
374 use super::*;
375 use crate::{AccountExternalAccountCreated, EventType};
376
377 const WEBHOOK_SECRET: &str = "secret";
378
379 #[test]
380 fn test_signature_parse() {
381 let raw_signature =
382 "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd";
383 let signature = Signature::parse(raw_signature).unwrap();
384 assert_eq!(signature.t, 1492774577);
385 assert_eq!(
386 signature.v1,
387 "5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
388 );
389
390 let raw_signature_with_test_mode = "t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39";
391 let signature = Signature::parse(raw_signature_with_test_mode).unwrap();
392 assert_eq!(signature.t, 1492774577);
393 assert_eq!(
394 signature.v1,
395 "5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
396 );
397 }
398
399 #[test]
400 fn test_generate_test_header() {
401 let payload = json!({
402 "id": "evt_test",
403 "object": "event",
404 "api_version": "2017-05-25",
405 "created": 1492774577,
406 "livemode": false,
407 "pending_webhooks": 1,
408 "data": {
409 "object": {
410 "object": "bank_account",
411 "country": "us",
412 "currency": "usd",
413 "id": "ba_test",
414 "last4": "6789",
415 "status": "verified",
416 }
417 },
418 "type": "account.external_account.created"
419 })
420 .to_string();
421 let secret = "whsec_test_secret";
422 let timestamp = 1492774577;
423
424 let signature = Webhook::generate_test_header(&payload, secret, Some(timestamp));
426
427 assert!(signature.starts_with("t=1492774577,v1="));
429
430 let event =
432 Webhook::construct_event_with_timestamp(&payload, &signature, secret, timestamp);
433 match event {
434 Ok(e) => {
435 assert_eq!(e.id.as_str(), "evt_test");
436 assert_eq!(e.type_, EventType::AccountExternalAccountCreated);
437 }
438 Err(e) => panic!("panic! {}", e),
439 }
440 }
441
442 #[test]
443 fn test_generate_test_header_integration() {
444 let payload = json!({
446 "id": "evt_test_webhook",
447 "object": "event",
448 "api_version": "2017-05-25",
449 "created": 1533204620,
450 "livemode": false,
451 "pending_webhooks": 1,
452 "data": {
453 "object": {
454 "object": "bank_account",
455 "country": "us",
456 "currency": "usd",
457 "id": "ba_test",
458 "last4": "6789",
459 "status": "verified",
460 }
461 },
462 "type": "account.external_account.created"
463 })
464 .to_string();
465
466 let secret = "whsec_test_secret";
467 let timestamp = Utc::now().timestamp();
468
469 let signature = Webhook::generate_test_header(&payload, secret, Some(timestamp));
471
472 let result =
474 Webhook::construct_event_with_timestamp(&payload, &signature, secret, timestamp);
475 assert!(result.is_ok());
476
477 let event = result.unwrap();
478 assert_eq!(event.id.as_str(), "evt_test_webhook");
479 assert_eq!(event.type_, EventType::AccountExternalAccountCreated);
480 }
481
482 fn get_mock_stripe_sig(msg: &str, timestamp: i64) -> String {
483 Webhook::generate_test_header(msg, WEBHOOK_SECRET, Some(timestamp))
484 }
485
486 fn mock_webhook_event(event_type: &EventType, data: Value) -> Value {
487 json!({
488 "id": "evt_123",
489 "object": "event",
490 "account": "acct_123",
491 "api_version": "2017-05-25",
492 "created": 1533204620,
493 "livemode": false,
494 "pending_webhooks": 1,
495 "request": {
496 "id": "req_123",
497 "idempotency_key": "idempotency-key-123"
498 },
499 "data": {
500 "object": data,
501 },
502 "type": event_type.to_string()
503 })
504 }
505
506 #[track_caller]
507 fn parse_mock_webhook_event(event_type: EventType, data: Value) -> EventObject {
508 let now = Utc::now().timestamp();
509 let payload = mock_webhook_event(&event_type, data).to_string();
510 let sig = get_mock_stripe_sig(&payload, now);
511
512 let webhook = Webhook { current_timestamp: now };
513 let parsed = webhook.do_construct_event(&payload, &sig, WEBHOOK_SECRET).unwrap();
514 assert_eq!(parsed.type_, event_type);
515 parsed.data.object
516 }
517
518 #[test]
519 #[cfg(feature = "async-stripe-billing")]
520 fn test_webhook_construct_event() {
521 let object = json!({
522 "id": "ii_123",
523 "object": "invoiceitem",
524 "amount": 1000,
525 "currency": "usd",
526 "customer": "cus_123",
527 "date": 1533204620,
528 "description": "Test Invoice Item",
529 "discountable": false,
530 "invoice": "in_123",
531 "livemode": false,
532 "metadata": {},
533 "period": {
534 "start": 1533204620,
535 "end": 1533204620
536 },
537 "proration": false,
538 "quantity": 3,
539 "quantity_decimal": "0"
540 });
541 let payload = mock_webhook_event(&EventType::InvoiceitemCreated, object);
542 let event_timestamp = 1533204620;
543 let signature = format!(
544 "t={event_timestamp},v1=d7373bc68f4bd320b253cd7461f87af6e1cdf1b4d7db1614d8d1d746972d2d0a,v0=63f3a72374a733066c4be69ed7f8e5ac85c22c9f0a6a612ab9a025a9e4ee7eef"
545 );
546
547 let webhook = Webhook { current_timestamp: event_timestamp };
548
549 let event = webhook
550 .do_construct_event(&payload.to_string(), &signature, WEBHOOK_SECRET)
551 .expect("Failed to construct event");
552
553 assert_eq!(event.type_, EventType::InvoiceitemCreated);
554 assert_eq!(event.id.as_str(), "evt_123",);
555 assert_eq!(event.account, "acct_123".parse().ok());
556 assert_eq!(event.created, 1533204620);
557
558 let EventObject::InvoiceitemCreated(invoice) = event.data.object else {
559 panic!("expected invoice item created");
560 };
561 assert_eq!(invoice.id.as_str(), "ii_123");
562 assert_eq!(invoice.quantity, 3);
563 }
564
565 #[cfg(feature = "async-stripe-billing")]
566 #[test]
567 fn test_billing_portal_session() {
569 let object = json!({
570 "configuration": "bpc_123",
571 "created": 1533204620,
572 "customer": "cus_123",
573 "id": "bps_123",
574 "livemode": false,
575 "url": "http://localhost:3000"
576 });
577 let result = parse_mock_webhook_event(EventType::BillingPortalSessionCreated, object);
578 let EventObject::BillingPortalSessionCreated(session) = result else {
579 panic!("expected billing portal session");
580 };
581 assert_eq!(session.url, "http://localhost:3000");
582 assert_eq!(session.id.as_str(), "bps_123");
583 assert_eq!(session.configuration.id().as_str(), "bpc_123");
584 }
585
586 #[test]
587 fn deserialize_polymorphic() {
588 let object = json!({
589 "object": "bank_account",
590 "country": "us",
591 "currency": "gbp",
592 "id": "ba_123",
593 "last4": "1234",
594 "status": "status",
595 });
596 let result = parse_mock_webhook_event(EventType::AccountExternalAccountCreated, object);
597 let EventObject::AccountExternalAccountCreated(bank_account) = result else {
598 panic!("unexpected type parsed");
599 };
600 let AccountExternalAccountCreated::BankAccount(bank_account) = *bank_account else {
601 panic!("unexpected type parsed");
602 };
603 assert_eq!(bank_account.id.as_str(), "ba_123");
604 assert_eq!(bank_account.last4, "1234");
605 }
606}