Security

The server-side proxy is only as secure as its weakest link. This guide covers the most common mistakes and how to avoid them.

User authorization

The single most important security consideration: where does the email come from? If an attacker can control the email value, they can generate a token for any user in your community.

Critical vulnerability

Never read the user's email from a query parameter, form field, or request body that the client controls. Always derive it from your verified server-side session.

Wrong

// WRONG  - anyone can request any user's token
app.get('/widget', async (req, res) => {
  const email = req.query.email;  // attacker sets ?email=admin@company.com
  // ...
});

Correct

// CORRECT  - email comes from a verified session
app.get('/widget', requireAuth, async (req, res) => {
  const email = req.user.email;  // from your auth middleware
  // ...
});

Production hardening

Protect the widget endpoint

The /widget endpoint returns authenticated HTML. It must be behind your auth middleware - the same way you protect any authenticated page.

Restrict CORS origins

If the widget endpoint is fetched via AJAX (e.g. loaded into a modal), restrict CORS to your own domain.

const cors = require('cors');

app.use('/widget', cors({
  origin: ['https://your-app.com'],
  credentials: true,
}));

Add rate limiting

Each widget request triggers two upstream API calls to Returning.AI. Rate-limit the endpoint to prevent abuse and protect your API quota.

const rateLimit = require('express-rate-limit');

const widgetLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 30,             // 30 requests per minute per IP
  message: { error: 'Too many requests' },
  standardHeaders: true,
  legacyHeaders: false,
});

app.get('/widget', widgetLimiter, requireAuth, async (req, res) => {
  // ...
});

Validate proxy paths (prevent SSRF)

If you allow the widget type or ID to vary, validate them against an allowlist. Never interpolate raw user input into the upstream URL.

// Validate widget type to prevent SSRF
const ALLOWED_TYPES = ['store', 'channel', 'milestone', 'social', 'currency', 'minigames'];

if (!ALLOWED_TYPES.includes(WIDGET_TYPE)) {
  return res.status(400).json({ error: 'Invalid widget type' });
}

// Never interpolate user input into the fetch URL
// WRONG: fetch(`${BASE}/${req.query.type}/${req.query.id}`)
// CORRECT: fetch(`${BASE}/${WIDGET_TYPE}/${WIDGET_ID}`)

Sanitize error messages

Internal error details (stack traces, upstream URLs, API key fragments) must never reach the client.

// Don't leak internal details to the client
app.get('/widget', requireAuth, async (req, res) => {
  try {
    // ... proxy logic
  } catch (error) {
    // Log the real error server-side
    console.error('Widget proxy error:', error);

    // Return a generic message to the client
    res.status(500).json({ error: 'Unable to load widget' });
  }
});

Security checklist

Review this list before going to production.

API error reference

Errors returned by the POST /widget/{communityId}/signin endpoint.

StatusMeaningCommon cause
401UnauthorizedMissing or invalid returningai-api-key header.
404Community not foundThe communityId in the URL does not match any active community.
404User not foundThe email header does not match a registered user. The user may need to be synced or created first.
429Rate limitedToo many signin requests. Back off and retry with exponential delay.
500Internal errorTransient server error. Retry once; if it persists, contact support.