Fix PCM auth: login for API token and multi webhook secrets.
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:
+54
-13
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user