Skip to main content

Overview

Conductor provides a unified authentication system that supports multiple authentication methods out of the box. Each method integrates seamlessly with the UnifiedRouter and can be configured globally, per-type, per-path, or per-member. Supported Authentication Methods:
  • Bearer Token (JWT) - For API authentication with JSON Web Tokens
  • API Key - For service-to-service authentication with KV storage
  • Cookie Session - For browser-based authentication with KV sessions
  • Unkey - Integration with Unkey.dev for managed API keys
  • Custom Validators - For webhook signatures (Stripe, GitHub, Twilio, etc.)

Bearer Token Authentication

Bearer token authentication uses JSON Web Tokens (JWT) for stateless API authentication.

Configuration

route:
  path: /api/protected
  auth:
    requirement: required
    methods: [bearer]

Environment Variables

# JWT Configuration
JWT_SECRET=your-secret-key-min-32-chars
JWT_ISSUER=https://your-domain.com
JWT_AUDIENCE=https://api.your-domain.com

Token Generation

Using Node.js (jose library)

import { SignJWT } from 'jose';

async function generateToken(userId: string, email: string) {
  const secret = new TextEncoder().encode(process.env.JWT_SECRET);

  const token = await new SignJWT({
    userId,
    email,
    roles: ['user'],
    permissions: ['read:profile', 'write:profile']
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('24h')
    .sign(secret);

  return token;
}

Using OpenSSL (for testing)

# Generate a secret key
openssl rand -base64 32

# Example token payload
{
  "userId": "user_123",
  "email": "user@example.com",
  "roles": ["user"],
  "permissions": ["read:profile"],
  "iss": "https://your-domain.com",
  "aud": "https://api.your-domain.com",
  "exp": 1735689600,
  "iat": 1735603200
}

Usage

Clients send the token in the Authorization header:
curl -H "Authorization: Bearer eyJhbGc..." https://api.example.com/protected

Token Validation

The Bearer validator automatically checks:
  • Token signature validity
  • Expiration time (exp claim)
  • Not-before time (nbf claim)
  • Issuer (iss claim)
  • Audience (aud claim)

Accessing User Context

In your member handlers:
export default async function handler(input: any, context: MemberContext) {
  const { auth } = context;

  if (auth.authenticated) {
    const userId = auth.user?.userId;
    const email = auth.user?.email;
    const roles = auth.user?.roles || [];
    const permissions = auth.user?.permissions || [];

    // Use authenticated user data
  }
}

Security Best Practices

  1. Use Strong Secrets: Minimum 32 characters, use openssl rand -base64 32
  2. Set Appropriate Expiration: 1 hour for sensitive operations, 24 hours for regular APIs
  3. Validate Issuer/Audience: Always set and validate to prevent token reuse
  4. Use HTTPS Only: Never send tokens over unencrypted connections
  5. Rotate Secrets Regularly: Implement secret rotation strategy
  6. Store Securely: Never log or expose tokens in error messages

API Key Authentication

API key authentication stores keys in Cloudflare KV for stateful service-to-service authentication.

Configuration

route:
  path: /api/service
  auth:
    requirement: required
    methods: [apiKey]

Environment Variables

# API Key Configuration
API_KEY_KV_NAMESPACE=API_KEYS  # Name of KV binding
API_KEY_HEADER=X-API-Key       # Header name (default: X-API-Key)

KV Namespace Setup

In wrangler.toml:
[[kv_namespaces]]
binding = "API_KEYS"
id = "your-kv-namespace-id"

Creating API Keys

API keys are stored in KV with metadata:
import { ApiKeyValidator } from '@ensemble-edge/conductor/auth';

async function createApiKey(env: any, keyData: {
  name: string;
  userId: string;
  permissions: string[];
  expiresIn?: number; // seconds
}) {
  const apiKey = crypto.randomUUID(); // or use a custom format

  const metadata = {
    name: keyData.name,
    userId: keyData.userId,
    permissions: keyData.permissions,
    createdAt: Date.now(),
    expiresAt: keyData.expiresIn ? Date.now() + (keyData.expiresIn * 1000) : undefined
  };

  await env.API_KEYS.put(
    `apikey:${apiKey}`,
    JSON.stringify(metadata),
    { expirationTtl: keyData.expiresIn }
  );

  return apiKey;
}

Usage

Clients can send API keys in multiple ways: Header (recommended):
curl -H "X-API-Key: your-api-key" https://api.example.com/service
Query parameter:
curl https://api.example.com/service?api_key=your-api-key
Cookie:
curl -b "api_key=your-api-key" https://api.example.com/service

Key Rotation

async function rotateApiKey(env: any, oldKey: string, userId: string) {
  // Get old key metadata
  const oldMetadata = await env.API_KEYS.get(`apikey:${oldKey}`, 'json');

  if (!oldMetadata || oldMetadata.userId !== userId) {
    throw new Error('Invalid key or unauthorized');
  }

  // Create new key with same permissions
  const newKey = await createApiKey(env, {
    name: oldMetadata.name,
    userId: oldMetadata.userId,
    permissions: oldMetadata.permissions
  });

  // Delete old key
  await env.API_KEYS.delete(`apikey:${oldKey}`);

  return newKey;
}

Security Best Practices

  1. Use UUIDs or Cryptographically Random Keys: Never use predictable keys
  2. Set Expiration: Always set TTL for keys (30-90 days recommended)
  3. Implement Key Rotation: Provide users ability to rotate keys
  4. Audit Key Usage: Log API key usage for security monitoring
  5. Rate Limit Per Key: Prevent abuse with per-key rate limits
  6. Revocation Strategy: Implement immediate key revocation when compromised
Cookie-based authentication for browser applications with server-side sessions stored in KV.

Configuration

route:
  path: /dashboard
  auth:
    requirement: required
    methods: [cookie]
    onFailure:
      action: redirect
      redirectTo: /login
      preserveReturn: true

Environment Variables

# Session Configuration
SESSION_KV_NAMESPACE=SESSIONS
SESSION_COOKIE_NAME=__session
SESSION_SECRET=your-secret-key-min-32-chars
SESSION_DOMAIN=.example.com
SESSION_MAX_AGE=86400  # 24 hours in seconds

KV Namespace Setup

In wrangler.toml:
[[kv_namespaces]]
binding = "SESSIONS"
id = "your-sessions-kv-id"

Creating Sessions

import { CookieSessionValidator } from '@ensemble-edge/conductor/auth';

async function login(email: string, password: string, env: any) {
  // Verify credentials (your auth logic)
  const user = await verifyCredentials(email, password);

  if (!user) {
    throw new Error('Invalid credentials');
  }

  // Create session
  const validator = new CookieSessionValidator({
    kvNamespace: 'SESSIONS',
    cookieName: '__session',
    secret: env.SESSION_SECRET,
    domain: '.example.com',
    maxAge: 86400
  });

  const sessionToken = await validator.createSession(env.SESSIONS, {
    userId: user.id,
    email: user.email,
    roles: user.roles,
    permissions: user.permissions,
    expiresAt: Date.now() + 86400000 // 24 hours
  });

  // Set cookie in response
  const response = new Response('Login successful', { status: 200 });
  response.headers.set(
    'Set-Cookie',
    `__session=${sessionToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400; Domain=.example.com`
  );

  return response;
}

Logging Out

async function logout(request: Request, env: any) {
  const validator = new CookieSessionValidator({
    kvNamespace: 'SESSIONS',
    cookieName: '__session',
    secret: env.SESSION_SECRET
  });

  // Get session token from cookie
  const cookies = request.headers.get('Cookie') || '';
  const sessionToken = cookies
    .split(';')
    .find(c => c.trim().startsWith('__session='))
    ?.split('=')[1];

  if (sessionToken) {
    await validator.deleteSession(env.SESSIONS, sessionToken);
  }

  // Clear cookie
  const response = new Response('Logged out', { status: 200 });
  response.headers.set(
    'Set-Cookie',
    '__session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0; Domain=.example.com'
  );

  return response;
}

Session Refresh

async function refreshSession(sessionToken: string, env: any) {
  const validator = new CookieSessionValidator({
    kvNamespace: 'SESSIONS',
    maxAge: 86400
  });

  await validator.refreshSession(env.SESSIONS, sessionToken, 86400);
}

Security Best Practices

  1. Always Use Secure Cookies: Set Secure flag in production
  2. HttpOnly Flag: Prevent XSS attacks by making cookies inaccessible to JavaScript
  3. SameSite Protection: Use SameSite=Lax or Strict to prevent CSRF
  4. Short Session Lifetime: 24 hours maximum, refresh on activity
  5. Session Invalidation: Delete sessions on logout and password change
  6. IP Binding (Optional): Store and validate IP address for additional security

Unkey Integration

Unkey.dev provides managed API key infrastructure with built-in analytics and rate limiting.

Configuration

route:
  path: /api/unkey-protected
  auth:
    requirement: required
    methods: [unkey]

Environment Variables

# Unkey Configuration
UNKEY_ROOT_KEY=unkey_root_xxxxx
UNKEY_API_ID=api_xxxxx
UNKEY_NAMESPACE=production  # Optional

Setup Unkey

  1. Sign up at unkey.dev
  2. Create an API:
    • Go to APIs section
    • Click “Create API”
    • Note your API ID
  3. Generate Root Key:
    • Go to Settings > Root Keys
    • Create new root key with permissions: api.*.read_key
    • Save the key securely

Creating Keys via Unkey Dashboard

  1. Go to your API in Unkey dashboard
  2. Click “Create Key”
  3. Set permissions, rate limits, and expiration
  4. Distribute key to users

Creating Keys Programmatically

async function createUnkeyApiKey(
  userId: string,
  permissions: string[],
  rateLimit?: { limit: number; duration: number }
) {
  const response = await fetch('https://api.unkey.dev/v1/keys.createKey', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${env.UNKEY_ROOT_KEY}`
    },
    body: JSON.stringify({
      apiId: env.UNKEY_API_ID,
      ownerId: userId,
      meta: { userId },
      ratelimit: rateLimit ? {
        type: 'fast',
        limit: rateLimit.limit,
        refillRate: rateLimit.limit,
        refillInterval: rateLimit.duration
      } : undefined,
      expires: Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
    })
  });

  const data = await response.json();
  return data.key;
}

Usage

curl -H "Authorization: Bearer unkey_xxxxx" https://api.example.com/unkey-protected

Key Verification

Unkey automatically tracks:
  • Key validity
  • Rate limit status
  • Usage analytics
  • Geographic distribution
View real-time analytics in the Unkey dashboard.

Security Best Practices

  1. Use Namespaces: Separate keys by environment (production, staging)
  2. Set Expiration: Always set key expiration dates
  3. Implement Rate Limits: Protect APIs with per-key rate limits
  4. Monitor Usage: Use Unkey analytics to detect abuse
  5. Revoke Compromised Keys: Immediately revoke in dashboard

Custom Validators

Custom validators enable webhook signature verification for third-party services.

Supported Services

  • Stripe - Webhook signature verification
  • GitHub - Webhook signature verification
  • Twilio - Request signature verification

Stripe Webhooks

Configuration

route:
  path: /webhooks/stripe
  methods: [POST]
  auth:
    requirement: required
    methods: [custom]
    customValidator: stripe

Environment Variables

STRIPE_WEBHOOK_SECRET=whsec_xxxxx

Setup

  1. Create webhook endpoint in Stripe Dashboard:
    • Go to Developers > Webhooks
    • Add endpoint: https://your-domain.com/webhooks/stripe
    • Select events to listen for
    • Copy webhook signing secret
  2. Configure in conductor.config.ts:
routing: {
  auth: {
    customValidators: {
      stripe: {
        type: 'stripe',
        secretEnvVar: 'STRIPE_WEBHOOK_SECRET',
        tolerance: 300 // 5 minutes
      }
    }
  }
}

GitHub Webhooks

Configuration

route:
  path: /webhooks/github
  methods: [POST]
  auth:
    requirement: required
    methods: [custom]
    customValidator: github

Environment Variables

GITHUB_WEBHOOK_SECRET=your-secret-from-github

Setup

  1. Create webhook in GitHub repository:
    • Go to Settings > Webhooks
    • Add webhook URL: https://your-domain.com/webhooks/github
    • Set Content type: application/json
    • Set Secret: generate random string
    • Select events
  2. Configure in conductor.config.ts:
routing: {
  auth: {
    customValidators: {
      github: {
        type: 'github',
        secretEnvVar: 'GITHUB_WEBHOOK_SECRET'
      }
    }
  }
}

Twilio Webhooks

Configuration

route:
  path: /webhooks/twilio
  methods: [POST]
  auth:
    requirement: required
    methods: [custom]
    customValidator: twilio

Environment Variables

TWILIO_AUTH_TOKEN=your-twilio-auth-token

Setup

  1. Configure webhook in Twilio Console:
    • Go to Phone Numbers
    • Select number
    • Set webhook URL for SMS/Voice: https://your-domain.com/webhooks/twilio
  2. Configure in conductor.config.ts:
routing: {
  auth: {
    customValidators: {
      twilio: {
        type: 'twilio',
        secretEnvVar: 'TWILIO_AUTH_TOKEN'
      }
    }
  }
}

Security Best Practices

  1. Always Verify Signatures: Never process webhooks without verification
  2. Use HTTPS: Webhooks must be delivered over secure connections
  3. Implement Replay Protection: Check timestamps and reject old requests
  4. Rate Limit Webhooks: Protect against webhook flooding
  5. Log All Webhooks: Audit trail for security and debugging
  6. Validate Payload Structure: Verify payload matches expected schema

Multi-Method Authentication

Routes can accept multiple authentication methods:
route:
  path: /api/flexible
  auth:
    requirement: required
    methods: [bearer, apiKey, unkey]  # Try each in order
The router will try each method in order until one succeeds.

Common Patterns

API with multiple client types:
# Mobile apps use bearer tokens, services use API keys
auth:
  methods: [bearer, apiKey]
Browser + API clients:
# Browsers use cookies, APIs use bearer tokens
auth:
  methods: [cookie, bearer]
Flexible public API:
# Accept any valid authentication, or anonymous
auth:
  requirement: optional
  methods: [bearer, apiKey, unkey]

Role-Based Access Control (RBAC)

Configuring Roles

route:
  path: /admin
  auth:
    requirement: required
    methods: [cookie, bearer]
    roles: [admin, superuser]  # User must have one of these roles

Setting Roles in Tokens

Bearer Token:
const token = await new SignJWT({
  userId: 'user_123',
  email: 'admin@example.com',
  roles: ['admin', 'user']
}).sign(secret);
Session:
const sessionToken = await validator.createSession(env.SESSIONS, {
  userId: user.id,
  email: user.email,
  roles: ['admin']
});
API Key:
await env.API_KEYS.put(`apikey:${apiKey}`, JSON.stringify({
  userId: user.id,
  roles: ['service_account']
}));

Permission-Based Access Control

Configuring Permissions

route:
  path: /api/billing
  auth:
    requirement: required
    methods: [bearer]
    permissions: [billing:read, billing:write]  # User must have ALL permissions

Setting Permissions

const token = await new SignJWT({
  userId: 'user_123',
  permissions: [
    'billing:read',
    'billing:write',
    'users:read'
  ]
}).sign(secret);

Rate Limiting

Per-User Rate Limiting

route:
  path: /api/expensive
  auth:
    requirement: required
    methods: [bearer]
    rateLimit:
      requests: 100
      window: 60  # 100 requests per 60 seconds
      keyBy: user

Per-IP Rate Limiting

route:
  path: /api/public
  auth:
    requirement: public
    rateLimit:
      requests: 1000
      window: 3600  # 1000 requests per hour
      keyBy: ip

Per-API-Key Rate Limiting

route:
  path: /api/service
  auth:
    requirement: required
    methods: [apiKey]
    rateLimit:
      requests: 10000
      window: 3600
      keyBy: apiKey

Failure Handling

Redirect to Login

route:
  auth:
    requirement: required
    methods: [cookie]
    onFailure:
      action: redirect
      redirectTo: /login
      preserveReturn: true  # Redirect back after login

Show Custom Error Page

route:
  auth:
    requirement: required
    roles: [admin]
    onFailure:
      action: page
      page: error-403
      context:
        message: "Admin access required"

Return JSON Error

route:
  auth:
    requirement: required
    methods: [bearer]
    onFailure:
      action: json
      status: 401
      body:
        error: unauthorized
        message: "Valid bearer token required"

Stealth Mode (404 Instead of 401)

route:
  auth:
    requirement: required
    methods: [bearer]
    stealthMode: true  # Returns 404 if unauthorized

Environment Variables Reference

Complete List

VariableAuth MethodRequiredDescription
JWT_SECRETBearerYesSecret key for JWT signing (min 32 chars)
JWT_ISSUERBearerNoExpected issuer claim in JWT
JWT_AUDIENCEBearerNoExpected audience claim in JWT
API_KEY_KV_NAMESPACEAPI KeyYesKV binding name for API keys
API_KEY_HEADERAPI KeyNoHeader name (default: X-API-Key)
SESSION_KV_NAMESPACECookieYesKV binding name for sessions
SESSION_COOKIE_NAMECookieNoCookie name (default: __session)
SESSION_SECRETCookieYesSecret for session token signing
SESSION_DOMAINCookieNoCookie domain
SESSION_MAX_AGECookieNoMax session age in seconds
UNKEY_ROOT_KEYUnkeyYesUnkey root key for API operations
UNKEY_API_IDUnkeyYesUnkey API ID
UNKEY_NAMESPACEUnkeyNoNamespace for key isolation
STRIPE_WEBHOOK_SECRETCustom (Stripe)YesStripe webhook signing secret
GITHUB_WEBHOOK_SECRETCustom (GitHub)YesGitHub webhook secret
TWILIO_AUTH_TOKENCustom (Twilio)YesTwilio auth token

Testing Authentication

Testing Bearer Tokens

# Generate test token
TOKEN=$(curl -X POST https://your-domain.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password"}' \
  | jq -r '.token')

# Use token
curl -H "Authorization: Bearer $TOKEN" https://your-domain.com/api/protected

Testing API Keys

curl -H "X-API-Key: your-api-key" https://your-domain.com/api/service

Testing Sessions

# Login and save cookie
curl -X POST https://your-domain.com/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password"}' \
  -c cookies.txt

# Use saved cookie
curl -b cookies.txt https://your-domain.com/dashboard

Testing Webhooks

# Stripe webhook
curl -X POST https://your-domain.com/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=1234567890,v1=signature" \
  -d '{"type":"payment_intent.succeeded"}'

# GitHub webhook
curl -X POST https://your-domain.com/webhooks/github \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=signature" \
  -d '{"action":"opened","pull_request":{}}'

Troubleshooting

”Invalid token” Error

Bearer Token:
  • Check JWT_SECRET matches signing secret
  • Verify token hasn’t expired
  • Check iss and aud claims if configured
API Key:
  • Verify key exists in KV namespace
  • Check key hasn’t expired
  • Ensure correct KV binding name

”Unauthorized” for Valid Credentials

  • Check auth method is listed in methods array
  • Verify environment variables are set correctly
  • Check KV namespace bindings in wrangler.toml
  • Enable audit logging to see validation details

Session Not Persisting

  • Verify SESSION_DOMAIN matches your domain
  • Check cookies are set with HttpOnly, Secure, SameSite
  • Ensure session hasn’t expired in KV
  • Verify cookie is being sent by browser

Webhook Signature Verification Failing

  • Check webhook secret matches exactly
  • Verify timestamp tolerance (default 5 minutes)
  • Ensure payload hasn’t been modified
  • Test with webhook testing tools provided by service