Fix PCM auth: login for API token and multi webhook secrets.
Deploy Beta / unit-tests (push) Successful in 8s
Deploy Beta / docker (push) Successful in 14s
Deploy Beta / deploy-beta (push) Successful in 1m34s

PCM DirectMail v3 needs POST /auth/login (apiKey+apiSecret) before design/order calls; accept each subscription's copy-only signature secret via PCM_WEBHOOK_SECRETS.
This commit is contained in:
2026-08-09 06:02:32 -05:00
parent 1ef6624a8b
commit 7496af72d0
8 changed files with 299 additions and 33 deletions
+54 -13
View File
@@ -1,3 +1,6 @@
import hashlib
import hmac
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@@ -74,19 +77,60 @@ def _campaign_report(campaign: Campaign) -> dict:
}
def _webhook_authorized(request, *, secret: str) -> bool:
secret = (secret or "").strip()
if not secret:
def _webhook_authorized(request, *, secret: str = "", secrets: list[str] | None = None) -> bool:
"""Accept Bearer / ?token= matching any configured secret (constant-time)."""
candidates: list[str] = []
if secrets:
candidates.extend(s.strip() for s in secrets if (s or "").strip())
single = (secret or "").strip()
if single and single not in candidates:
candidates.append(single)
if not candidates:
return True
token = (request.GET.get("token") or "").strip()
auth = (request.headers.get("Authorization") or "").strip()
if token and token == secret:
return True
if auth.lower().startswith("bearer ") and auth[7:].strip() == secret:
return True
bearer = auth[7:].strip() if auth.lower().startswith("bearer ") else ""
# Common signature-header names PCM / gateways may use (raw secret or HMAC).
sig_headers = (
request.headers.get("X-PCM-Signature")
or request.headers.get("X-Webhook-Signature")
or request.headers.get("X-Signature")
or request.headers.get("X-Hub-Signature-256")
or ""
).strip()
if sig_headers.lower().startswith("sha256="):
sig_headers = sig_headers[7:].strip()
body = request.body or b""
for candidate in candidates:
if token and hmac.compare_digest(token, candidate):
return True
if bearer and hmac.compare_digest(bearer, candidate):
return True
if sig_headers:
if hmac.compare_digest(sig_headers, candidate):
return True
digest = hmac.new(
candidate.encode("utf-8"), body, hashlib.sha256
).hexdigest()
if hmac.compare_digest(sig_headers, digest):
return True
return False
def _pcm_webhook_secrets() -> list[str]:
"""All PCM subscription signature secrets from env."""
raw_list = (getattr(settings, "PCM_WEBHOOK_SECRETS", None) or "").strip()
single = (getattr(settings, "PCM_WEBHOOK_SECRET", None) or "").strip()
out: list[str] = []
if raw_list:
out.extend(p.strip() for p in raw_list.split(",") if p.strip())
if single and single not in out:
out.append(single)
return out
@login_required
@require_http_methods(["GET", "POST"])
def campaign_list(request):
@@ -453,14 +497,11 @@ def postcard_webhook(request):
"""
PCM Integrations order / mail-tracking webhook.
Configure in PCM → Webhooks:
Configure in PCM → Webhooks (one subscription per event):
URL: https://<host>/portal/messaging/webhooks/postcard/
Authorization: Bearer + PCM_WEBHOOK_SECRET
Events: order / recipient status updates (Delivered, Undeliverable, …)
Copy each subscription's signature secret into PCM_WEBHOOK_SECRETS
"""
if not _webhook_authorized(
request, secret=settings.PCM_WEBHOOK_SECRET or ""
):
if not _webhook_authorized(request, secrets=_pcm_webhook_secrets()):
return HttpResponseForbidden("invalid webhook token")
payload = parse_webhook_payload(request)
if not payload: