Task 1 (Solo Recon) · The Meridian Pivot sprint · Northstar Retail Co. (Sprint 2) Unfamiliar tool learned: Webhook verification via HMAC-SHA256 signatures.
A minimal Flask service that receives webhook POST requests and verifies their
authenticity using an HMAC-SHA256 signature. This proves two things about every
incoming request:
- Origin — the sender holds the shared secret (not just anyone can forge it).
- Integrity — the payload has not been altered since it was signed.
This prototype is the solo-learning deliverable for Assignment 1. It also lays the groundwork for the team's Day 4 pivot, when Northstar switches its inventory sync from a polling model to a webhook push model.
Northstar wants a live inventory sync service so their support tool's "is this in stock?" answers stay accurate.
- Original spec (polling / pull): ask the warehouse API every 5 minutes.
- Pivot spec (webhook / push): the warehouse pushes a notification the moment stock changes — and every pushed message must be verified before we trust it.
Verifying those pushes is exactly what this prototype does.
Sender Receiver (this service)
------ -----------------------
signature = HMAC(secret, payload)
POST /webhook ─────────────────────────▶ recomputed = HMAC(secret, raw_body)
header: X-Signature: <signature> compare_digest(recomputed, received)
match → 200 {"status": "valid"}
mismatch→ 401 {"status": "invalid"}
missing → 401 {"error": "..."}
Key security detail: comparison uses hmac.compare_digest() (constant-time) to
prevent timing attacks, rather than a plain ==.
webhooks-verification/
├── app.py # Flask server: /webhook endpoint + HMAC verification
├── test_webhook.py # Test harness: valid / tampered / missing-header cases
├── Blocker_Journal.md # Learning & Blocker Journal (Assignment 1 deliverable)
├── README.md # This file
└── .gitignore
- Python 3.10+
pip install flask requests
python3 app.py
# Serving on http://127.0.0.1:5000python3 test_webhook.py| Test | Scenario | Status |
|---|---|---|
| 1 | Valid signature | 200 — {"message": "Signature verified", "status": "valid"} |
| 2 | Tampered payload (signature for original body) | 401 — {"message": "Signature mismatch", "status": "invalid"} |
| 3 | Missing signature header | 401 — {"error": "Missing signature header"} |
| Setting | Current (prototype) | Production recommendation |
|---|---|---|
| Shared secret | Hard-coded constant | Read from os.environ.get("WEBHOOK_SECRET") |
| Debug mode | debug=True |
False behind a WSGI server (gunicorn/uwsgi) |
| Replay protection | None | Validate a timestamp header, reject if > 5 min old |
- Secret is hard-coded — move to an environment variable.
- No timestamp/replay protection yet.
- No key-rotation / multiple-signature support.
- Development server only (not production-hardened).
These are intentionally out of scope for the Day 1–2 solo prototype and are logged
in Blocker_Journal.md under "If I had more time."
Alex — Task 1, The Meridian Pivot sprint.