Skip to main content

JWT Tokens Guide

Learn how to implement server-side authentication using JWT tokens for secure backend-to-backend communication with Fanfare.

Overview

JWT (JSON Web Token) authentication allows your backend to securely authenticate consumers with Fanfare without client-side credential exposure. This is the recommended approach for production applications with existing user accounts. What you’ll learn:
  • Understanding Fanfare’s JWT authentication flow
  • Implementing server-side token exchange
  • Validating admission tokens
  • Securing your integration
Complexity: Advanced Time to complete: 45 minutes

Prerequisites

  • Fanfare account with API credentials including secret key
  • Backend server (Node.js, Python, Go, etc.)
  • Basic understanding of JWT and authentication concepts

When to Use JWT Authentication

Use JWT authentication when:
  • Your application has existing user accounts
  • You need server-to-server authentication
  • You want to validate admission tokens before checkout
  • You require enhanced security for high-value transactions

Authentication Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                          Your Infrastructure                          │
│                                                                       │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐            │
│  │   Browser   │     │  Your API   │     │  Checkout   │            │
│  │   (SDK)     │     │   Server    │     │   Server    │            │
│  └─────────────┘     └─────────────┘     └─────────────┘            │
│         │                   │                   │                    │
└─────────│───────────────────│───────────────────│────────────────────┘
          │                   │                   │
          │  1. User logs in  │                   │
          │─────────────────►│                   │
          │                   │                   │
          │                   │ 2. Exchange Code  │
          │                   │──────────────────►│─────┐
          │                   │                   │     │ Fanfare API
          │                   │◄──────────────────│◄────┘
          │                   │                   │
          │  3. Exchange Code │                   │
          │◄─────────────────│                   │
          │                   │                   │
          │  4. exchangeExternal()                │
          │───────────────────────────────────────│─────┐
          │                   │                   │     │ Fanfare API
          │◄──────────────────────────────────────│◄────┘
          │   (Fanfare Session)                   │
          │                   │                   │
          │  5. User Admitted │                   │
          │───────────────────────────────────────│─────┐
          │                   │                   │     │ Experience
          │◄──────────────────────────────────────│◄────┘
          │   (Admission Token)                   │
          │                   │                   │
          │  6. Checkout with Token               │
          │───────────────────┼──────────────────►│
          │                   │                   │
          │                   │ 7. Validate Token │
          │                   │◄──────────────────│
          │                   │                   │─────┐
          │                   │──────────────────►│     │ Fanfare API
          │                   │◄──────────────────│◄────┘
          │                   │ (Valid/Invalid)   │

Step 1: External Authentication Endpoint

Create a backend endpoint to generate exchange codes:

Node.js/Express

// routes/fanfare-auth.ts
import express from "express";

const router = express.Router();

// Environment variables
const FANFARE_API_URL = process.env.FANFARE_API_URL || "https://api.fanfare.io";
const FANFARE_ORG_ID = process.env.FANFARE_ORG_ID!;
const FANFARE_SECRET_KEY = process.env.FANFARE_SECRET_KEY!;

interface ExternalAuthPayload {
  provider: string;
  issuer: string;
  subject: string;
  claims?: Record<string, unknown>;
}

// POST /api/fanfare/authorize
router.post("/authorize", async (req, res) => {
  try {
    // 1. Verify the user is authenticated in YOUR system
    const user = req.user; // From your auth middleware
    if (!user) {
      return res.status(401).json({ error: "Unauthorized" });
    }

    // 2. Create external auth payload
    const payload: ExternalAuthPayload = {
      provider: "your-platform", // Unique identifier for your platform
      issuer: "https://your-domain.com", // Your domain
      subject: user.id, // User's unique ID in your system
      claims: {
        email: user.email,
        name: user.name,
        // Add any other relevant user data
        tier: user.subscriptionTier,
        createdAt: user.createdAt,
      },
    };

    // 3. Request exchange code from Fanfare
    const response = await fetch(`${FANFARE_API_URL}/auth/external/authorize`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Organization-Id": FANFARE_ORG_ID,
        "X-Secret-Key": FANFARE_SECRET_KEY, // Server-side only!
      },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      const error = await response.json();
      console.error("Fanfare auth error:", error);
      return res.status(500).json({ error: "Authentication failed" });
    }

    const { exchangeCode, expiresAt } = await response.json();

    // 4. Return exchange code to client
    // The code expires in 60 seconds
    res.json({ exchangeCode, expiresAt });
  } catch (error) {
    console.error("External auth error:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});

export default router;

Python/FastAPI

# routes/fanfare_auth.py
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
import httpx
import os

router = APIRouter()

FANFARE_API_URL = os.getenv("FANFARE_API_URL", "https://api.fanfare.io")
FANFARE_ORG_ID = os.getenv("FANFARE_ORG_ID")
FANFARE_SECRET_KEY = os.getenv("FANFARE_SECRET_KEY")

class AuthResponse(BaseModel):
    exchangeCode: str
    expiresAt: str

@router.post("/authorize", response_model=AuthResponse)
async def authorize_fanfare(user: User = Depends(get_current_user)):
    """Generate Fanfare exchange code for authenticated user."""

    payload = {
        "provider": "your-platform",
        "issuer": "https://your-domain.com",
        "subject": str(user.id),
        "claims": {
            "email": user.email,
            "name": user.name,
        }
    }

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{FANFARE_API_URL}/auth/external/authorize",
            json=payload,
            headers={
                "Content-Type": "application/json",
                "X-Organization-Id": FANFARE_ORG_ID,
                "X-Secret-Key": FANFARE_SECRET_KEY,
            }
        )

        if response.status_code != 200:
            raise HTTPException(status_code=500, detail="Fanfare auth failed")

        return response.json()

Step 2: Client-Side Exchange

Exchange the code in the browser:
import { useFanfare, useFanfareAuth } from "@waitify-io/fanfare-sdk-react";
import { useEffect } from "react";

interface User {
  id: string;
  email: string;
}

function useFanfareExternalAuth(user: User | null) {
  const fanfare = useFanfare();
  const { isAuthenticated, session } = useFanfareAuth();

  useEffect(() => {
    async function authenticate() {
      if (!user || isAuthenticated) return;

      try {
        // 1. Get exchange code from your backend
        const response = await fetch("/api/fanfare/authorize", {
          method: "POST",
          credentials: "include", // Include auth cookies
        });

        if (!response.ok) {
          throw new Error("Failed to get exchange code");
        }

        const { exchangeCode } = await response.json();

        // 2. Exchange for Fanfare session
        await fanfare.auth.exchangeExternal({ exchangeCode });

        console.log("Fanfare authentication successful");
      } catch (error) {
        console.error("Fanfare auth failed:", error);
      }
    }

    authenticate();
  }, [user, isAuthenticated, fanfare]);

  return { isAuthenticated, session };
}

Step 3: Token Validation Endpoint

Validate admission tokens during checkout:

Node.js/Express

// routes/validate-admission.ts
import express from "express";

const router = express.Router();

interface ValidationRequest {
  queueId?: string;
  drawId?: string;
  consumerId: string;
  token: string;
}

// POST /api/validate-admission
router.post("/validate-admission", async (req, res) => {
  try {
    const { queueId, drawId, consumerId, token } = req.body as ValidationRequest;

    // Determine endpoint based on distribution type
    let endpoint: string;
    if (queueId) {
      endpoint = `${FANFARE_API_URL}/queues/${queueId}/validate`;
    } else if (drawId) {
      endpoint = `${FANFARE_API_URL}/draws/${drawId}/validate`;
    } else {
      return res.status(400).json({ error: "queueId or drawId required" });
    }

    // Validate with Fanfare
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Organization-Id": FANFARE_ORG_ID,
        "X-Secret-Key": FANFARE_SECRET_KEY,
      },
      body: JSON.stringify({ consumerId, token }),
    });

    if (!response.ok) {
      const error = await response.json();
      return res.status(403).json({
        valid: false,
        error: error.error || "Token validation failed",
      });
    }

    const isValid = await response.json();

    if (!isValid) {
      return res.status(403).json({
        valid: false,
        error: "Invalid or expired admission token",
      });
    }

    // Token is valid - allow checkout to proceed
    res.json({ valid: true });
  } catch (error) {
    console.error("Validation error:", error);
    res.status(500).json({ error: "Validation failed" });
  }
});

export default router;

Step 4: Secure Checkout Flow

Validate admission before processing checkout:
// routes/checkout.ts
import express from "express";

const router = express.Router();

interface CheckoutRequest {
  admissionToken: string;
  consumerId: string;
  experienceId: string;
  experienceType: "queue" | "draw" | "auction";
  distributionId: string;
  cart: CartItem[];
}

router.post("/checkout", async (req, res) => {
  const { admissionToken, consumerId, experienceType, distributionId, cart } = req.body as CheckoutRequest;

  // 1. Validate admission token with Fanfare
  const validationResult = await validateAdmission({
    type: experienceType,
    distributionId,
    consumerId,
    token: admissionToken,
  });

  if (!validationResult.valid) {
    return res.status(403).json({
      error: "Invalid admission",
      message: "Your access has expired or is invalid",
      code: "ADMISSION_INVALID",
    });
  }

  // 2. Process the order
  try {
    const order = await processOrder({
      consumerId,
      cart,
      admissionToken,
    });

    // 3. Complete the admission (marks it as used)
    await completeAdmission({
      type: experienceType,
      distributionId,
      consumerId,
    });

    res.json({
      success: true,
      orderId: order.id,
    });
  } catch (error) {
    res.status(500).json({
      error: "Checkout failed",
      message: error instanceof Error ? error.message : "Unknown error",
    });
  }
});

async function validateAdmission(params: { type: string; distributionId: string; consumerId: string; token: string }) {
  const endpoint =
    params.type === "queue"
      ? `/queues/${params.distributionId}/validate`
      : params.type === "draw"
        ? `/draws/${params.distributionId}/validate`
        : `/auctions/${params.distributionId}/validate`;

  const response = await fetch(`${FANFARE_API_URL}${endpoint}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": FANFARE_ORG_ID,
      "X-Secret-Key": FANFARE_SECRET_KEY,
    },
    body: JSON.stringify({
      consumerId: params.consumerId,
      token: params.token,
    }),
  });

  if (!response.ok) {
    return { valid: false };
  }

  return { valid: await response.json() };
}

async function completeAdmission(params: { type: string; distributionId: string; consumerId: string }) {
  const endpoint =
    params.type === "queue"
      ? `/queues/${params.distributionId}/complete`
      : params.type === "draw"
        ? `/draws/${params.distributionId}/complete`
        : `/auctions/${params.distributionId}/complete`;

  await fetch(`${FANFARE_API_URL}${endpoint}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Organization-Id": FANFARE_ORG_ID,
      "X-Secret-Key": FANFARE_SECRET_KEY,
    },
    body: JSON.stringify({
      consumerId: params.consumerId,
    }),
  });
}

export default router;

Step 5: JWT Token Structure

Understanding the tokens Fanfare uses:

Access Token Payload

interface AccessTokenPayload {
  consumerId: string;
  organizationId: string;
  sessionId: string;
  email?: string;
  phone?: string;
  isGuest?: boolean;
  iat: number; // Issued at
  exp: number; // Expiration
  iss: string; // Issuer: "consumer-app"
}

Admission Token Payload

interface AdmissionTokenPayload {
  consumerId: string;
  distributionId: string;
  distributionType: "queue" | "draw" | "auction";
  position?: number; // For queues
  admittedAt: string;
  expiresAt: string;
  fingerprint?: string;
}

Step 6: Security Best Practices

Protect Your Secret Key

// ❌ Don't do this
const response = await fetch("/api/fanfare", {
  headers: {
    "X-Secret-Key": "sk_live_xxx", // NEVER expose in client code
  },
});

// ✅ Do this - server-side only
// Your backend handles all secret key operations
const response = await fetch("/your-backend/api/fanfare/authorize");

Validate Token Origin

// Verify the admission originated from expected experience
async function validateAdmissionContext(token: string, expectedExperienceId: string) {
  const decoded = decodeAdmissionToken(token);

  if (decoded.experienceId !== expectedExperienceId) {
    throw new Error("Token does not match expected experience");
  }

  return decoded;
}

Implement Rate Limiting

import rateLimit from "express-rate-limit";

const authLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 10, // 10 requests per minute
  message: { error: "Too many auth requests" },
});

router.post("/authorize", authLimiter, async (req, res) => {
  // ... handler
});

Validate Claims

function validateExternalClaims(claims: Record<string, unknown>) {
  // Ensure claims don't exceed size limit
  const claimsJson = JSON.stringify(claims);
  if (claimsJson.length > 10000) {
    throw new Error("Claims too large");
  }

  // Sanitize sensitive data
  const sanitized = { ...claims };
  delete sanitized.password;
  delete sanitized.ssn;
  delete sanitized.creditCard;

  return sanitized;
}

Step 7: Error Handling

Handle common authentication errors:
async function handleFanfareAuth(user: User) {
  try {
    const { exchangeCode } = await fetch("/api/fanfare/authorize", {
      method: "POST",
      credentials: "include",
    }).then((r) => r.json());

    await fanfare.auth.exchangeExternal({ exchangeCode });
  } catch (error) {
    if (error instanceof Error) {
      switch (error.message) {
        case "exchange_code_expired":
          // Code expired (60 second TTL)
          // Retry with new code
          return handleFanfareAuth(user);

        case "invalid_exchange_code":
          // Code already used or invalid
          console.error("Invalid exchange code");
          break;

        case "consumer_conflict":
          // External identity already linked to different consumer
          showConflictResolution();
          break;

        default:
          console.error("Auth error:", error.message);
      }
    }
  }
}

Complete Integration Example

Backend (Node.js)

// app.ts
import express from "express";
import fanfareAuthRouter from "./routes/fanfare-auth";
import checkoutRouter from "./routes/checkout";

const app = express();

app.use(express.json());
app.use("/api/fanfare", fanfareAuthRouter);
app.use("/api", checkoutRouter);

app.listen(3000);

Frontend (React)

// App.tsx
function App() {
  const { user } = useYourAuth();

  return (
    <FanfareProvider organizationId="org_xxx" publishableKey="pk_live_xxx">
      <FanfareAuthSync user={user} />
      <YourApp />
    </FanfareProvider>
  );
}

function FanfareAuthSync({ user }: { user: User | null }) {
  useFanfareExternalAuth(user);
  return null;
}

Troubleshooting

Exchange Code Expired

  • Codes expire after 60 seconds
  • Generate new code immediately before exchange
  • Check for network latency issues

Secret Key Errors

  • Verify key has sk_ prefix (not pk_)
  • Check key permissions in dashboard
  • Ensure key is for correct environment

Token Validation Fails

  • Verify consumer ID matches
  • Check token hasn’t expired
  • Ensure fingerprint matches (if enabled)

What’s Next