Security Full Closed Beta
This page describes the security model for the FanFest Loyalty Egress API, covering API key provisioning, webhook signature verification, replay attack protection, SSRF protection, and secret rotation guidance.
Closed Beta
The Loyalty Egress API is in Closed Beta. The security model described here is subject to change as the system evolves. Contact us for access.
API Key Provisioning
All egress REST API endpoints require an API key with the egress:read permission. API keys are managed in the API Keys section of your channel admin panel.
Creating an API Key
- Navigate to Admin → API Keys in your channel dashboard
- Click Create API Key and give it a descriptive name (e.g.,
loyalty-egress-production) - Enable the
egress:readpermission — this grants read access to loyalty data via the egress REST API - Copy the generated key immediately — it is shown only once
- Store the key securely (environment variable or secrets manager) and never commit it to version control
Permission Scope
The egress:read permission grants access to:
GET /egress/loyalty/actions— query loyalty actions for your channelGET /egress/loyalty/points— get total loyalty points for a userGET /egress/loyalty/config— check your channel's egress configuration
API keys without egress:read receive a 403 Forbidden response when accessing these endpoints. Missing or invalid keys receive 401 Unauthorized.
Passing the API Key
Include the key in the x-api-key header on every request:
curl -H "x-api-key: your_api_key_here" \
https://api.fanfest.vip/egress/loyalty/actionsSignature Verification Algorithm
Every webhook delivery is signed with HMAC-SHA256. You must verify signatures before processing any payload to ensure the request originated from FanFest and was not tampered with in transit.
For complete code examples, see Signature Verification in the Webhooks guide.
Step-by-Step Signing Process
FanFest signs each delivery as follows:
- Capture the current Unix timestamp (seconds since epoch) at the moment of delivery
- Construct the signed content by concatenating the timestamp and the raw JSON payload with a period separator:
<timestamp>.<json_body> - Compute HMAC-SHA256 of the signed content using your webhook secret as the key, producing a hex-encoded digest
- Assemble the signature header in the format
t=<timestamp>,v1=<hex_hmac>and send it asX-FanFest-Signature
Step-by-Step Verification Process
On your end, verify the signature by reversing the signing process:
- Parse the
X-FanFest-Signatureheader — split on,, then on=to extracttandv1 - Reconstruct the signed content:
<t>.<raw_request_body>(use the raw bytes — do not parse JSON first) - Compute HMAC-SHA256 of the signed content using your webhook secret
- Compare using a timing-safe function — always use
crypto.timingSafeEqual(Node.js) orhmac.compare_digest(Python); never use===or== - Check the timestamp — see Replay Protection below
Use raw request body
Compute the HMAC over the raw request bytes, not the JSON-parsed value. Any whitespace difference will cause verification to fail. In Express, use express.raw({ type: 'application/json' }) before the route handler to preserve the original body.
Timing-Safe Comparison
FanFest's server-side implementation uses buffer padding to ensure timing safety, avoiding both length leakage and RangeError from mismatched buffer sizes:
// Server-side approach — safe even when lengths differ
const computedBuf = Buffer.from(expectedSignature, 'utf8');
const receivedBuf = Buffer.alloc(computedBuf.length); // always 64 bytes for SHA-256 hex
Buffer.from(receivedSignature || '', 'utf8').copy(receivedBuf);
const isValid = timingSafeEqual(computedBuf, receivedBuf);
if (!isValid) throw new Error('Webhook signature verification failed');This avoids the common mistake of length-checking before timingSafeEqual, which itself leaks timing information when the lengths differ.
Replay Protection
The X-FanFest-Timestamp header (and the t field in X-FanFest-Signature) contains the Unix timestamp of when the signature was generated. Timestamp validation is built into the signature verification algorithm to prevent replay attacks.
Rejection Windows
FanFest's own verification code rejects timestamps outside the following window:
- Older than 5 minutes (300 seconds) — protects against captured-and-replayed deliveries
- More than 60 seconds in the future — accounts for minor clock drift between systems
The check in code:
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > 300 || age < -60) {
// Reject — timestamp is outside acceptable window
}Implementing Replay Protection
Apply the same window in your verification logic:
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > maxAgeSeconds || age < -60) {
throw new Error('Webhook signature timestamp is too old or in the future');
}Clock synchronization
Ensure your server clock is synchronized via NTP. A drifted server clock will cause legitimate deliveries to be rejected if the clock skew exceeds 60 seconds.
See the complete verification example in the Webhooks guide, which includes both timestamp checking and timing-safe comparison.
SSRF Protection
Webhook URLs are validated at registration time to prevent Server-Side Request Forgery (SSRF) attacks. The validation enforces:
Blocklist (Exact Hostname Match)
The following hostnames are blocked at registration time, regardless of what they resolve to:
| Hostname | Reason |
|---|---|
localhost | Loopback alias |
127.0.0.1 | IPv4 loopback |
::1 | IPv6 loopback |
169.254.169.254 | AWS EC2 instance metadata endpoint |
0.0.0.0 | Unspecified address |
CIDR Range Blocking (via DNS Resolution)
After passing the hostname blocklist, FanFest resolves the hostname via DNS and checks the resolved IP address against the following ranges:
| Range | CIDR | Description |
|---|---|---|
| Loopback | 127.0.0.0/8 | All 127.x.x.x addresses |
| Link-local | 169.254.0.0/16 | AWS metadata, link-local |
| RFC-1918 | 10.0.0.0/8 | Class A private range |
| RFC-1918 | 172.16.0.0/12 | 172.16.x.x–172.31.x.x private range |
| RFC-1918 | 192.168.0.0/16 | Class C private range |
The DNS check fails closed — if the hostname cannot be resolved, the URL is rejected.
Additional Requirements
- HTTPS required — HTTP webhook URLs are rejected at registration time
- Validation is creation-time only — URL validation runs when the webhook is created (via the
create()path in the repository). Theupdate()path currently does not re-validate the URL. If self-service URL modification is added in the future, validation must be added to the update path as well.
Why creation-time only?
DNS-based SSRF protection is inherently a point-in-time check — a hostname could be re-pointed to an internal IP after registration (DNS rebinding). This limitation is documented here for transparency. Future work may add delivery-time validation or DNS rebinding mitigations.
Secret Rotation
Webhook signing secrets are generated with a minimum of 32 bytes of cryptographic randomness (format: whsec_<64 hex characters>). If you suspect a secret has been compromised, follow the steps below to rotate it.
Rotation Process (Closed Beta)
During Closed Beta, secret rotation is a coordinated process with the FanFest team — you cannot rotate secrets self-service. Contact support@fanfest.vip to initiate rotation.
The recommended rotation procedure is:
- Contact the FanFest team — request a new webhook endpoint with a new signing secret. Provide your channel ID and the webhook URL you want registered (this can be the same URL as your existing webhook)
- Verify delivery — once the new webhook is provisioned, confirm that test events are delivered successfully to your endpoint with the new secret
- Support both secrets temporarily — update your verification code to accept signatures from either the old secret or the new secret during the transition period:javascript
function verifyWithFallback(payload, signatureHeader, primarySecret, legacySecret) { try { verifyWebhookSignature(payload, primarySecret, signatureHeader); return true; } catch { // Fall back to legacy secret during transition verifyWebhookSignature(payload, legacySecret, signatureHeader); return true; } } - Disable the old webhook — ask the FanFest team to disable the previous webhook endpoint
- Remove the old secret — once confirmed that no further deliveries are coming from the old webhook, remove the old secret from your code and secrets manager
Self-service rotation
This section will be updated when self-service webhook management is available. During Closed Beta, the FanFest team handles all webhook provisioning and secret rotation.
Secret Storage Best Practices
- Store secrets in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) — never in version control
- Restrict access to secrets to only the services that need them
- Rotate secrets if they are accidentally exposed (logged, committed to git, included in an error message, etc.)
- Never expose secrets in client-side code or API responses
