> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sayna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Protect Sayna endpoints with either a shared secret or delegated JWT validation.

Sayna supports two authentication strategies. Choose the one that matches your deployment needs, or enable both. When both are configured Sayna first checks the API secret, then falls back to delegated JWT validation.

Protected HTTP endpoints accept the authentication token via either method:

* **Header**: `Authorization: Bearer <token>`
* **Query parameter**: `?api_key=<token>`

Both API Secret mode and JWT mode use the same token extraction behavior.

## Token precedence and fallback

When both an `Authorization` header and an `api_key` query parameter are present, Sayna evaluates them in this order:

1. The `Authorization` header is evaluated first.
2. If the header format is invalid, the server falls back to the `api_key` query parameter.

If neither source provides a usable token, the request is rejected with `missing_auth_header`.

## Quick summary

| Mode                   | When to use                                   | Highlights                                                                                                |
| ---------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **API secret**         | Single-tenant deployments or trusted networks | Lowest latency, no external service, bearer-token comparison against `AUTH_API_SECRET`.                   |
| **JWT + auth service** | Multi-tenant or context-aware authorization   | Sayna signs the entire request payload, calls out to your service, and trusts the HTTP status you return. |

<Callout>
  Toggle auth with the `AUTH_REQUIRED` environment variable. When it's `false`, all endpoints accept unauthenticated requests.
</Callout>

## Option A — API secret authentication

### Setup steps

1. Generate a long random secret.

   ```bash theme={null}
   openssl rand -base64 32
   ```

2. Set the environment variables:

   ```bash theme={null}
   AUTH_REQUIRED=true
   AUTH_API_SECRET=your-generated-secret
   ```

3. Clients send the secret as a bearer token in the header or as a query parameter:

   ```bash theme={null}
   # Using Authorization header (recommended)
   curl -X POST http://localhost:3001/speak \
     -H "Authorization: Bearer your-generated-secret" \
     -H "Content-Type: application/json" \
     -d '{"text": "Hello world"}'

   # Using query parameter
   curl -X POST "http://localhost:3001/speak?api_key=your-generated-secret" \
     -H "Content-Type: application/json" \
     -d '{"text": "Hello world"}'
   ```

### Security notes

* Use at least 32 random bytes; rotate periodically and never commit secrets to VCS.
* Always terminate TLS before traffic reaches Sayna so the bearer token stays encrypted in transit.
* Pair with IP allowlists when you use this mode for back-office tools.

## Option B — Delegated JWT authentication

API secret not enough? Let your own service validate tokens and return `200`/`401` decisions.

### 1. Generate signing keys

```bash theme={null}
# RSA
openssl genrsa -out auth_private_key.pem 2048
openssl rsa -in auth_private_key.pem -pubout -out auth_public_key.pem
chmod 600 auth_private_key.pem
chmod 644 auth_public_key.pem

# or ECDSA
openssl ecparam -genkey -name prime256v1 -noout -out auth_private_key.pem
openssl ec -in auth_private_key.pem -pubout -out auth_public_key.pem
```

### 2. Configure Sayna

```bash theme={null}
AUTH_REQUIRED=true
AUTH_SERVICE_URL=https://your-auth-service.com/auth
AUTH_SIGNING_KEY_PATH=/path/to/auth_private_key.pem
AUTH_TIMEOUT_SECONDS=5
```

### 3. Implement the auth service

Your service must:

1. Accept `POST` requests whose body is the JWT Sayna generated.
2. Verify the JWT signature using the public key (RS256 or ES256).
3. Validate the extracted bearer token and any contextual data.
4. Return `200 OK` (allow) or `401 Unauthorized` (deny). Other 4xx map to `401`, 5xx become `502`, and timeouts surface as `503` upstream.

```javascript theme={null}
const express = require('express');
const jwt = require('jsonwebtoken');
const fs = require('fs');

const app = express();
const publicKey = fs.readFileSync('auth_public_key.pem');

app.post('/auth', express.text(), (req, res) => {
  try {
    const payload = jwt.verify(req.body, publicKey, { algorithms: ['RS256', 'ES256'] });
    const isValid = validateToken(payload.token);
    if (isValid) {
      res.status(200).send('OK');
    } else {
      res.status(401).send('Invalid token');
    }
  } catch (error) {
    res.status(401).send('JWT verification failed');
  }
});

app.listen(3000);
```

## Environment variables

| Variable                | Required    | Default | Description                                       |
| ----------------------- | ----------- | ------- | ------------------------------------------------- |
| `AUTH_REQUIRED`         | No          | `false` | Enable or disable authentication middleware.      |
| `AUTH_API_SECRET`       | Conditional | –       | Shared secret for API secret auth.                |
| `AUTH_SERVICE_URL`      | Conditional | –       | External auth service endpoint (JWT mode).        |
| `AUTH_SIGNING_KEY_PATH` | Conditional | –       | Path to private key used to sign the JWT payload. |
| `AUTH_TIMEOUT_SECONDS`  | No          | `5`     | Timeout for auth service requests.                |

When `AUTH_REQUIRED=true` you must specify either `AUTH_API_SECRET` or both `AUTH_SERVICE_URL` and `AUTH_SIGNING_KEY_PATH`. You can configure both to let Sayna short-circuit known service-to-service traffic with the secret and route user traffic through the JWT flow.

## JWT payload schema (delegated mode)

```json theme={null}
{
  "sub": "sayna-auth",
  "iat": 1234567890,
  "exp": 1234568190,
  "auth_data": {
    "token": "extracted-token-from-header-or-query",
    "request_body": {"text": "request body as JSON"},
    "request_headers": {
      "content-type": "application/json",
      "user-agent": "client-agent"
    },
    "request_path": "/speak",
    "request_method": "POST"
  }
}
```

* `sub`: Always `sayna-auth` so your service knows who issued the JWT.
* `iat` / `exp`: Issued-at plus 5-minute expiration.
* `auth_data.token`: The extracted usable token, resolved from the `Authorization` header or the `api_key` query parameter (following the precedence rule above).
* `auth_data.request_body`: JSON payload of the HTTP request.
* `auth_data.request_headers`: Filtered headers (excludes `authorization`, `cookie`, `x-forwarded-*`, `x-sayna-*`, `host`, `x-real-ip`).
* `auth_data.request_path` / `request_method`: Full request context for policy decisions.

### Verification checklist

```python theme={null}
def verify_auth_request(jwt_string, public_key):
    payload = jwt.decode(jwt_string, public_key, algorithms=['RS256', 'ES256'])
    if payload['sub'] != 'sayna-auth':
        return 401, 'Invalid JWT subject'
    now = int(time.time())
    if abs(now - payload['iat']) > 60:
        return 401, 'JWT too old'
    auth_data = payload['auth_data']
    bearer_token = auth_data['token']
    if not is_valid_token(bearer_token):
        return 401, 'Invalid bearer token'
    return 200, 'OK'
```

## Protected endpoints

When `AUTH_REQUIRED=true` the following endpoints require authentication:

* `POST /speak`
* `GET /voices`
* `POST /livekit/token`
* `GET /livekit/rooms`
* `GET /livekit/rooms/{room_name}`
* `DELETE /livekit/participant`
* `POST /livekit/participant/mute`
* `GET /recording/{stream_id}`
* `POST /sip/call`
* `GET /sip/hooks`
* `POST /sip/hooks`
* `DELETE /sip/hooks`
* `POST /sip/transfer`

Public endpoints today:

* `GET /` (health)
* `GET /ws` (WebSocket handshake — see the WebSocket Auth roadmap below)

## Room-scoped authorization

Beyond authentication, room-scoped endpoints enforce ownership via `metadata.auth_id`:

| Endpoint                         | Ownership check                                                                            |
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| `POST /livekit/token`            | Creates room with `metadata.auth_id` if missing; returns `403` if owned by another tenant. |
| `GET /livekit/rooms`             | Filters to rooms matching `metadata.auth_id`.                                              |
| `GET /livekit/rooms/{room_name}` | Returns `404` if `metadata.auth_id` doesn't match.                                         |
| `DELETE /livekit/participant`    | Returns `404` if `metadata.auth_id` doesn't match.                                         |
| `POST /livekit/participant/mute` | Returns `404` if `metadata.auth_id` doesn't match.                                         |
| `POST /sip/call`                 | Creates room with `metadata.auth_id`; returns `404` if owned by another tenant.            |
| `POST /sip/transfer`             | Returns `404` if `metadata.auth_id` doesn't match.                                         |

<Info>
  Room names are no longer prefixed or modified. Access control is enforced entirely through the `metadata.auth_id` field stored in room metadata.
</Info>

## WebSocket authentication status

WebSocket auth is not implemented yet. The open tasks from the design document:

1. Authenticate during the HTTP upgrade by reading query parameters or headers.
2. Require an auth payload inside the first `config` message.
3. Or leave `/ws` public but scope LiveKit mirroring to trusted rooms.

Today, Sayna runs with **Option C** (no WebSocket auth). Use network isolation or API Gateways until JWT enforcement ships.

## Usage examples

```bash theme={null}
# API secret — header
curl -X GET http://localhost:3001/voices \
  -H "Authorization: Bearer $AUTH_API_SECRET"

# API secret — query parameter
curl -X GET "http://localhost:3001/voices?api_key=$AUTH_API_SECRET"

# JWT-based — header
curl -X POST http://localhost:3001/speak \
  -H "Authorization: Bearer user-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{"text": "Different user"}'

# JWT-based — query parameter
curl -X POST "http://localhost:3001/speak?api_key=user-jwt-token" \
  -H "Content-Type: application/json" \
  -d '{"text": "Different user"}'
```

## Client guidance

**Recommended default**: Use the `Authorization: Bearer <token>` header whenever possible. Header-based auth keeps tokens out of URL strings, server logs, and browser history.

**When to use query parameter auth**: The `?api_key=<token>` query parameter is practical in environments where adding custom headers is difficult — for example, browser redirects, webhook callbacks from third-party services, or server-sent event streams.

<Warning>
  Tokens passed as query parameters can be logged in URL logs, proxy access logs, and analytics tooling. Always use HTTPS and avoid logging full URLs when query auth is used.
</Warning>

## Backward compatibility

* Existing header-based clients continue to work unchanged.
* Query-based auth is additive and non-breaking.
* Clients currently sending an invalid `Authorization` header alongside a valid `api_key` query parameter will now authenticate successfully via fallback.

## Scope clarification

This dual-input auth behavior applies to **protected HTTP endpoints only**. It does not change:

* The **WebSocket auth model** (see [WebSocket authentication status](#websocket-authentication-status) below).
* The **LiveKit webhook signature verification flow**, which uses its own signing mechanism independent of bearer tokens.

<Info>
  **Provider-level credentials** are separate from server-level authentication. In addition to the API secret and JWT strategies described above, Sayna supports per-session and per-request provider credential overrides via the `auth` field in STT/TTS config objects. This allows multi-tenant clients to supply their own Deepgram, ElevenLabs, Cartesia, Google, or Azure credentials without changing server configuration. See the [WebSocket guide](/guides/websocket#provider-auth-overrides) for details.
</Info>

## Error responses

```json theme={null}
{
  "error": "error_code",
  "message": "Human-readable error message"
}
```

| Error code                 | HTTP status | Description                                                                                      |
| -------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
| `missing_auth_header`      | 401         | No usable token was found in either the `Authorization` header or the `api_key` query parameter. |
| `invalid_auth_header`      | 401         | Header format is invalid and no valid `api_key` fallback was provided.                           |
| `unauthorized`             | 401         | Token validation failed.                                                                         |
| `auth_service_error`       | 401 or 502  | Auth service returned 4xx/5xx.                                                                   |
| `auth_service_unavailable` | 503         | Auth service unreachable or timed out.                                                           |
| `config_error`             | 500         | Misconfigured auth flags/keys.                                                                   |
| `jwt_signing_error`        | 500         | Failed to sign JWT payload.                                                                      |

### Mapping auth service responses

| Auth service status     | Sayna response            | Meaning                                           |
| ----------------------- | ------------------------- | ------------------------------------------------- |
| `200 OK`                | `200 OK`                  | Token valid; request proceeds.                    |
| `401 Unauthorized`      | `401 Unauthorized`        | Invalid token; client should refresh credentials. |
| Other `4xx`             | `401 Unauthorized`        | Client error mapped to unauthorized.              |
| `5xx`                   | `502 Bad Gateway`         | Temporary auth service failure.                   |
| Timeout / network error | `503 Service Unavailable` | Auth service unreachable.                         |

## Security considerations

* **Private keys**: Store private keys with `chmod 600`, never commit them, and rotate periodically.
* **Network security**: Run Sayna + auth service over HTTPS/TLS. Consider mutual TLS between the two if you run them across trust zones.
* **Token validation**: Reject replayed JWTs by checking `iat`; combine with short expirations.
* **Error handling**: Return generic error bodies to clients but log detailed errors internally for auditing.
* **Replay protection**: Compare `iat` against `now` (±60 seconds) before trusting the payload.

<Warning>
  Always verify the JWT signature **and** the embedded request hash before trusting delegated responses. Skipping either check makes it possible to replay tampered requests.
</Warning>
