Browse Source

Stripe forward webhook

David Burke 6 days ago
parent
commit
d7db5b4ae1
4 changed files with 34 additions and 12 deletions
  1. 2 2
      apps/stripe/schema.py
  2. 24 10
      apps/stripe/views.py
  3. 3 0
      glitchtip/settings.py
  4. 5 0
      glitchtip/urls.py

+ 2 - 2
apps/stripe/schema.py

@@ -103,8 +103,8 @@ class EventData(BaseModel):
 
 
 class StripeEventRequest(BaseModel):
-    id: str | None
-    idempotency_key: str | None
+    id: str | None = None
+    idempotency_key: str | None = None
 
 
 class StripeEvent(BaseModel):

+ 24 - 10
apps/stripe/views.py

@@ -2,6 +2,7 @@ import hmac
 import logging
 import time
 
+import aiohttp
 from django.conf import settings
 from django.core.cache import cache
 from django.http import (
@@ -57,7 +58,7 @@ async def update_price(price: Price):
     )
 
 
-async def update_subscription(subscription: Subscription):
+async def update_subscription(subscription: Subscription, request: HttpRequest):
     customer_obj = Customer.model_validate_json(
         await stripe_get(f"customers/{subscription.customer}")
     )
@@ -73,10 +74,19 @@ async def update_subscription(subscription: Subscription):
         return
 
     # Check region, is it this region or should it be forwarded
-    region = customer_metadata.get("region")
+    region = customer_metadata.get("region", "")
     if region != settings.STRIPE_REGION:
-        domain = settings.STRIPE_REGION.get(region)
-        # Forward entire request
+        forward_url = settings.STRIPE_REGION_DOMAINS.get(region)
+        headers = {
+            "Stripe-Signature": request.headers.get("Stripe-Signature"),
+            "Content-Type": "application/json",
+        }
+        async with aiohttp.ClientSession() as session:
+            async with session.post(
+                forward_url, data=request.body, headers=headers, ssl=True
+            ) as response:
+                await response.read()
+        return
 
     organization = await Organization.objects.filter(id=organization_id).afirst()
     if not organization:
@@ -110,7 +120,7 @@ async def update_subscription(subscription: Subscription):
 
 @csrf_exempt
 @require_POST
-async def stripe_webhook_view(request: HttpRequest):
+async def stripe_webhook_view(request: HttpRequest, event_type: str | None = None):
     """
     Handles Stripe webhook events.
 
@@ -125,7 +135,7 @@ async def stripe_webhook_view(request: HttpRequest):
         return HttpResponseForbidden("Missing signature header")
 
     try:
-        if not verify_stripe_signature(payload, sig_header):
+        if not verify_stripe_signature(payload, sig_header, event_type):
             logger.warning("Stripe webhook signature verification failed.")
             return HttpResponseForbidden("Invalid signature")
     except ValueError as e:
@@ -144,7 +154,7 @@ async def stripe_webhook_view(request: HttpRequest):
         return HttpResponse(status=200)
 
     if idempotency_key := event.request.idempotency_key:
-        if cache.get_or_set("stripe" + idempotency_key, True, 60):
+        if not cache.add("stripe" + idempotency_key, None, 60):
             return HttpResponse(status=200)
 
     if event.type in ["product.updated", "product.created"]:
@@ -153,7 +163,7 @@ async def stripe_webhook_view(request: HttpRequest):
         "customer.subscription.updated",
         "customer.subscription.created",
     ]:
-        await update_subscription(event.data.object)
+        await update_subscription(event.data.object, request)
     elif event.type in ["price.updated", "price.created"]:
         await update_price(event.data.object)
     else:
@@ -162,7 +172,7 @@ async def stripe_webhook_view(request: HttpRequest):
     return HttpResponse(status=200)
 
 
-def verify_stripe_signature(payload, sig_header):
+def verify_stripe_signature(payload, sig_header, event_type: str):
     """Verifies the Stripe webhook signature.
 
     Args:
@@ -175,7 +185,11 @@ def verify_stripe_signature(payload, sig_header):
         ValueError: if the signature header is malformed.
     """
 
-    webhook_secret = getattr(settings, "STRIPE_WEBHOOK_SECRET", None)
+    webhook_secret = (
+        settings.STRIPE_WEBHOOK_SECRET_SUBSCRIPTION
+        if event_type == "subscription"
+        else settings.STRIPE_WEBHOOK_SECRET
+    )
     if not webhook_secret:
         logger.error("STRIPE_WEBHOOK_SECRET not configured in settings.")
         #  Return False/raise exception based on desired behavior (security vs. failing fast).

+ 3 - 0
glitchtip/settings.py

@@ -330,6 +330,9 @@ BILLING_ENABLED = False
 STRIPE_PUBLIC_KEY = env.str("STRIPE_PUBLIC_KEY", None)
 STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY", None)
 STRIPE_WEBHOOK_SECRET = env.str("STRIPE_WEBHOOK_SECRET", None)
+STRIPE_WEBHOOK_SECRET_SUBSCRIPTION = env.str(
+    "STRIPE_WEBHOOK_SECRET_SUBSCRIPTION", STRIPE_WEBHOOK_SECRET
+)
 STRIPE_REGION = env.str("STRIPE_REGION", "")  # Sets stripe customer metadata
 STRIPE_REGION_DOMAINS = env.dict(
     "STRIPE_REGION_DOMAINS", default={}

+ 5 - 0
glitchtip/urls.py

@@ -33,6 +33,11 @@ urlpatterns = [
     ),
     path("api/", api.urls),
     path("stripe/webhook/", stripe_webhook_view, name="stripe_webhook"),
+    path(
+        "stripe/webhook/<str:event_type>/",
+        stripe_webhook_view,
+        name="stripe_webhook_with_type",
+    ),
 ]
 
 if "django.contrib.admin" in settings.INSTALLED_APPS: