Skip to main content
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.

Quick summary

ModeWhen to useHighlights
API secretSingle-tenant deployments or trusted networksLowest latency, no external service, bearer-token comparison against AUTH_API_SECRET.
JWT + auth serviceMulti-tenant or context-aware authorizationSayna signs the entire request payload, calls out to your service, and trusts the HTTP status you return.
Toggle auth with the AUTH_REQUIRED environment variable. When it’s false, all endpoints accept unauthenticated requests.

Option A — API secret authentication

Setup steps

  1. Generate a long random secret.
    openssl rand -base64 32
    
  2. Set the environment variables:
    AUTH_REQUIRED=true
    AUTH_API_SECRET=your-generated-secret
    
  3. Clients send the secret as a bearer token:
    curl -X POST http://localhost:3001/speak \
      -H "Authorization: Bearer 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

# RSA
oopenssl genrsa -out auth_private_key.pem 2048
oopenssl 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
oopenssl ecparam -genkey -name prime256v1 -noout -out auth_private_key.pem
oopenssl ec -in auth_private_key.pem -pubout -out auth_public_key.pem

2. Configure Sayna

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

VariableRequiredDefaultDescription
AUTH_REQUIREDNofalseEnable or disable authentication middleware.
AUTH_API_SECRETConditionalShared secret for API secret auth.
AUTH_SERVICE_URLConditionalExternal auth service endpoint (JWT mode).
AUTH_SIGNING_KEY_PATHConditionalPath to private key used to sign the JWT payload.
AUTH_TIMEOUT_SECONDSNo5Timeout 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)

{
  "sub": "sayna-auth",
  "iat": 1234567890,
  "exp": 1234568190,
  "auth_data": {
    "token": "bearer-token-from-auth-header",
    "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: Bearer token extracted from the Authorization header.
  • 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

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
  • Any future REST routes you add to the protected router
Public endpoints today:
  • GET / (health)
  • GET /ws (WebSocket handshake — see the WebSocket Auth roadmap below)

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

# API secret
curl -X GET http://localhost:3001/voices \
  -H "Authorization: Bearer $AUTH_API_SECRET"

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

Error responses

{
  "error": "error_code",
  "message": "Human-readable error message"
}
Error codeHTTP statusDescription
missing_auth_header401Authorization header missing.
invalid_auth_header401Header format not Bearer <token>.
unauthorized401Token validation failed.
auth_service_error401 or 502Auth service returned 4xx/5xx.
auth_service_unavailable503Auth service unreachable or timed out.
config_error500Misconfigured auth flags/keys.
jwt_signing_error500Failed to sign JWT payload.

Mapping auth service responses

Auth service statusSayna responseMeaning
200 OK200 OKToken valid; request proceeds.
401 Unauthorized401 UnauthorizedInvalid token; client should refresh credentials.
Other 4xx401 UnauthorizedClient error mapped to unauthorized.
5xx502 Bad GatewayTemporary auth service failure.
Timeout / network error503 Service UnavailableAuth 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.
Always verify the JWT signature and the embedded request hash before trusting delegated responses. Skipping either check makes it possible to replay tampered requests.