Developer Blog

Authentication Deep Dive: OAuth, API Keys, and JWTs

Authentication Deep Dive: OAuth, API Keys, and JWTs

Authentication in voice AI has unique challenges. Users can't type passwords mid-conversation. Sessions may span hours. Multiple devices need seamless handoff.

This guide covers everything you need to secure your Ferni integration.

Authentication Methods Overview

MethodBest ForSecurity LevelComplexity
API KeysServer-to-serverMediumLow
OAuth 2.0User-facing appsHighMedium
JWTMicroservicesHighMedium
SAML/SSOEnterpriseHighestHigh

API Keys: Simple but Effective

For server-side integrations, API keys are straightforward:

import { FerniClient } from '@ferni/sdk';

const client = new FerniClient({
  apiKey: process.env.FERNI_API_KEY,
});

Security Best Practices

Never expose API keys client-side. Use a backend proxy:

// ❌ Bad: API key in browser
const client = new FerniClient({ apiKey: 'sk_live_...' });

// ✅ Good: Proxy through your backend
const client = new FerniClient({
  endpoint: '/api/ferni-proxy',
  // Your backend adds the API key
});

Rotate keys regularly:

# Generate new key
ferni keys:create --name "production-v2"

# Update your environment
export FERNI_API_KEY=sk_live_new_key

# Revoke old key after deployment
ferni keys:revoke sk_live_old_key

Use key scopes:

// Create a key with limited permissions
const key = await ferni.keys.create({
  name: 'mobile-app',
  scopes: ['conversations:read', 'conversations:write'],
  // Restrict to specific IP ranges
  allowedIps: ['203.0.113.0/24'],
  // Auto-expire
  expiresAt: new Date('2026-06-01'),
});

OAuth 2.0: For User-Facing Apps

When users log into your app with Ferni, use OAuth 2.0.

Authorization Code Flow

// 1. Redirect user to Ferni auth
const authUrl = client.getAuthorizationUrl({
  clientId: process.env.FERNI_CLIENT_ID,
  redirectUri: 'https://yourapp.com/callback',
  scopes: ['profile', 'conversations', 'insights'],
  state: generateSecureState(),
});

// 2. Handle callback
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state to prevent CSRF
  if (!verifyState(state)) {
    return res.status(400).send('Invalid state');
  }

  // Exchange code for tokens
  const tokens = await client.exchangeCode({
    code,
    redirectUri: 'https://yourapp.com/callback',
  });

  // Store tokens securely
  await storeTokens(userId, tokens);

  res.redirect('/dashboard');
});

Token Refresh

Access tokens expire. Handle refresh automatically:

const client = new FerniClient({
  clientId: process.env.FERNI_CLIENT_ID,
  clientSecret: process.env.FERNI_CLIENT_SECRET,
  tokens: await getStoredTokens(userId),
  // Refresh 5 minutes before expiry
  refreshThreshold: 300,
  onTokenRefresh: async (newTokens) => {
    await storeTokens(userId, newTokens);
  },
});

JWTs: For Microservices

When your backend services talk to Ferni, JWTs provide secure, stateless authentication.

Generating JWTs

import jwt from 'jsonwebtoken';

function generateFerniToken(userId: string, claims: object) {
  return jwt.sign(
    {
      sub: userId,
      iss: 'your-app',
      aud: 'ferni-api',
      ...claims,
    },
    process.env.JWT_SECRET,
    {
      algorithm: 'RS256',
      expiresIn: '1h',
    }
  );
}

// Use with client
const client = new FerniClient({
  auth: {
    type: 'jwt',
    token: generateFerniToken(userId, { tier: 'premium' }),
  },
});

JWT Best Practices

  1. Use RS256, not HS256 - Asymmetric keys allow verification without exposing secrets
  2. Keep tokens short-lived - 1 hour max for access tokens
  3. Include minimal claims - Only what's needed for authorization
  4. Validate on every request - Never cache JWT validation results

Multi-Tenant Authentication

For SaaS apps serving multiple organizations:

const client = new FerniClient({
  apiKey: process.env.FERNI_API_KEY,
  // Isolate data per tenant
  tenantId: user.organizationId,
  // Tenant-specific settings
  tenantConfig: {
    // Custom persona for this tenant
    defaultPersona: tenant.ferniPersonaId,
    // Tenant's webhook endpoint
    webhookUrl: tenant.webhookUrl,
  },
});

Data Isolation

Ferni automatically isolates tenant data:

// User from Org A
const clientA = new FerniClient({ tenantId: 'org-a', ... });
await clientA.memory.store({ key: 'preference', value: 'blue' });

// User from Org B
const clientB = new FerniClient({ tenantId: 'org-b', ... });
await clientB.memory.get('preference'); // Returns null, not 'blue'

Enterprise SSO (SAML)

For enterprise customers requiring SAML:

// Configure SAML provider
await ferni.sso.configure({
  tenantId: 'enterprise-customer',
  provider: 'saml',
  samlConfig: {
    entryPoint: 'https://idp.customer.com/sso/saml',
    issuer: 'ferni-app',
    cert: process.env.SAML_CERT,
    // Map SAML attributes to Ferni user
    attributeMapping: {
      email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
      name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
      groups: 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups',
    },
  },
});

Voice-Specific Auth Challenges

Challenge 1: No Keyboard Input

Users can't type passwords during voice sessions. Solutions:

// Use voice biometrics for re-auth
const verificationResult = await client.verifyVoice({
  audio: voiceSample,
  userId: currentUser.id,
});

if (verificationResult.confidence > 0.95) {
  // Proceed with sensitive operation
}

Challenge 2: Shared Devices

Multiple family members might use the same smart speaker:

// Voice identification before sensitive actions
client.on('sensitive_action', async (action, context) => {
  const speaker = await context.identifySpeaker();

  if (speaker.confidence < 0.8) {
    return {
      response: "I want to make sure I'm talking to the right person. " +
                "Can you verify with your PIN?",
      waitForPin: true,
    };
  }

  return { proceed: true };
});

Challenge 3: Long Sessions

Voice sessions can last hours. Keep auth fresh:

const client = new FerniClient({
  // Silently refresh tokens during natural pauses
  auth: {
    type: 'oauth',
    tokens: userTokens,
    refreshStrategy: 'opportunistic',
    // Refresh when user pauses for 3+ seconds
    refreshOnPause: 3000,
  },
});

Security Checklist

Before going to production:

  • [ ] API keys stored in environment variables, not code
  • [ ] HTTPS only for all endpoints
  • [ ] Webhook signatures verified on every request
  • [ ] Tokens refreshed before expiry
  • [ ] Sensitive actions require re-authentication
  • [ ] Rate limiting on auth endpoints
  • [ ] Audit logging for auth events
  • [ ] Key rotation procedure documented

Next Steps

Questions? Join us on Discord.