> Legacy approach
>
> For new integrations, use the [Widget SDK with Access Key Embed](https://docs.returning.ai/widget-sdk/auth-access-key.md).

# 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

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

```javascript
// 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.

```javascript
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.

```javascript
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.

```javascript
// Validate widget type to prevent SSRF
const ALLOWED_TYPES = ['store', 'channel', 'milestone', 'social', 'currency-view', 'referral-conditions', 'custom'];

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.

```javascript
// 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 key stored in environment variables, not in source code.env file added to .gitignore Widget endpoint requires authentication middleware User email derived from verified server-side session HTTPS enforced in production Rate limiting applied to the widget endpoint CORS restricted to your application domain(s) Error messages sanitized - no internal details exposed Debug/test endpoints removed before deploy

## API error reference

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

| Status | Meaning | Common cause |
| --- | --- | --- |
| `401` | Unauthorized | Missing or invalid `returningai-api-key` header. |
| `404` | Community not found | The `communityId` in the URL does not match any active community. |
| `404` | User not found | The `email` header does not match a registered user. The user may need to be synced or created first. |
| `429` | Rate limited | Too many signin requests. Back off and retry with exponential delay. |
| `500` | Internal error | Transient server error. Retry once; if it persists, contact support. |
