Skip to content

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

  1. Navigate to Admin → API Keys in your channel dashboard
  2. Click Create API Key and give it a descriptive name (e.g., loyalty-egress-production)
  3. Enable the egress:read permission — this grants read access to loyalty data via the egress REST API
  4. Copy the generated key immediately — it is shown only once
  5. 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 channel
  • GET /egress/loyalty/points — get total loyalty points for a user
  • GET /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:

bash
curl -H "x-api-key: your_api_key_here" \
  https://api.fanfest.vip/egress/loyalty/actions

Signature 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:

  1. Capture the current Unix timestamp (seconds since epoch) at the moment of delivery
  2. Construct the signed content by concatenating the timestamp and the raw JSON payload with a period separator: <timestamp>.<json_body>
  3. Compute HMAC-SHA256 of the signed content using your webhook secret as the key, producing a hex-encoded digest
  4. Assemble the signature header in the format t=<timestamp>,v1=<hex_hmac> and send it as X-FanFest-Signature

Step-by-Step Verification Process

On your end, verify the signature by reversing the signing process:

  1. Parse the X-FanFest-Signature header — split on ,, then on = to extract t and v1
  2. Reconstruct the signed content: <t>.<raw_request_body> (use the raw bytes — do not parse JSON first)
  3. Compute HMAC-SHA256 of the signed content using your webhook secret
  4. Compare using a timing-safe function — always use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python); never use === or ==
  5. 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:

javascript
// 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:

javascript
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:

javascript
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:

HostnameReason
localhostLoopback alias
127.0.0.1IPv4 loopback
::1IPv6 loopback
169.254.169.254AWS EC2 instance metadata endpoint
0.0.0.0Unspecified 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:

RangeCIDRDescription
Loopback127.0.0.0/8All 127.x.x.x addresses
Link-local169.254.0.0/16AWS metadata, link-local
RFC-191810.0.0.0/8Class A private range
RFC-1918172.16.0.0/12172.16.x.x172.31.x.x private range
RFC-1918192.168.0.0/16Class 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). The update() 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:

  1. 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)
  2. Verify delivery — once the new webhook is provisioned, confirm that test events are delivered successfully to your endpoint with the new secret
  3. 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;
      }
    }
  4. Disable the old webhook — ask the FanFest team to disable the previous webhook endpoint
  5. 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

Released under the MIT License.