Skip to content

Authentication

z0 provides API key parsing and tenant extraction, but does not verify keys against a database. This is intentional—the SDK is generic and doesn’t prescribe how you store or verify credentials.

The authenticateRequest() function in @z0-app/sdk:

  1. Extracts the X-API-Key header
  2. Validates the key format: <prefix>_<mode>_<tenant_id>_<random>
  3. Returns the parsed components (mode, tenant_id, apiKey)
import { authenticateRequest } from '@z0-app/sdk';
export async function handleRequest(request: Request) {
const authResult = authenticateRequest(request, 'myapp');
if (!authResult.success) {
return new Response(authResult.error, { status: 401 });
}
const { tenantId, mode, apiKey } = authResult.context;
// Now you must verify the key...
}

You are responsible for:

Store API keys securely. Options include:

  • KV Namespace: Fast lookups, good for high-volume APIs
  • D1 Database: Relational storage with metadata
  • External Auth Service: Auth0, Clerk, or custom service

Example KV schema:

Key: apikey:<hash_of_key>
Value: { tenant_id: "...", created_at: ..., scopes: [...] }

After parsing, verify the key exists and is valid:

async function verifyApiKey(apiKey: string, env: Env): Promise<boolean> {
// Hash the key for lookup (never store plain keys)
const keyHash = await hashApiKey(apiKey);
// Look up in your storage
const record = await env.API_KEYS.get(keyHash);
if (!record) return false;
const data = JSON.parse(record);
// Check if key is revoked
if (data.revoked_at) return false;
// Check if key has expired
if (data.expires_at && data.expires_at < Date.now()) return false;
return true;
}

Ensure the tenant_id in the key matches a real tenant:

async function verifyTenant(tenantId: string, env: Env): Promise<boolean> {
// Check against your tenant registry
const tenant = await env.TENANTS.get(tenantId);
return tenant !== null;
}

The mode field (live/test) should affect behavior:

if (authResult.context.mode === 'test') {
// Route to test data, sandbox environment
// Use mock external services
// Apply lower rate limits
}
import { authenticateRequest, type AuthContext } from '@z0-app/sdk';
interface Env {
API_KEYS: KVNamespace;
TENANTS: KVNamespace;
}
export async function authenticate(
request: Request,
env: Env
): Promise<AuthContext | Response> {
// Step 1: Parse the API key
const authResult = authenticateRequest(request, 'myapp');
if (!authResult.success) {
return new Response(JSON.stringify({ error: authResult.error }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const { apiKey, tenantId, mode } = authResult.context;
// Step 2: Verify the key exists and is valid
const keyHash = await hashApiKey(apiKey);
const keyRecord = await env.API_KEYS.get(keyHash);
if (!keyRecord) {
return new Response(JSON.stringify({ error: 'Invalid API key' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
const keyData = JSON.parse(keyRecord);
if (keyData.revoked_at) {
return new Response(JSON.stringify({ error: 'API key revoked' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// Step 3: Verify tenant exists
const tenant = await env.TENANTS.get(tenantId);
if (!tenant) {
return new Response(JSON.stringify({ error: 'Unknown tenant' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// Step 4: Verify key belongs to this tenant
if (keyData.tenant_id !== tenantId) {
return new Response(JSON.stringify({ error: 'Key/tenant mismatch' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
// Success - return the auth context
return authResult.context;
}
async function hashApiKey(key: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Always hash keys before storage:

// Good: Store hash
const hash = await hashApiKey(plainKey);
await env.API_KEYS.put(hash, JSON.stringify(metadata));
// Bad: Store plain key
await env.API_KEYS.put(plainKey, JSON.stringify(metadata)); // DON'T

When comparing keys or hashes, use constant-time comparison to prevent timing attacks:

import { timingSafeEqual } from 'crypto';
function secureCompare(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return timingSafeEqual(bufA, bufB);
}

Protect against brute-force attacks:

const rateLimitKey = `ratelimit:auth:${clientIP}`;
const attempts = await env.RATE_LIMITS.get(rateLimitKey);
if (attempts && parseInt(attempts) > 10) {
return new Response('Too many attempts', { status: 429 });
}

Create Facts for security auditing:

await ledger.appendFact({
type: 'auth',
subtype: success ? 'login_success' : 'login_failure',
tenant_id: tenantId,
data: {
ip: request.headers.get('CF-Connecting-IP'),
user_agent: request.headers.get('User-Agent'),
key_prefix: apiKey.substring(0, 10) + '...'
}
});

The standard format is:

<prefix>_<mode>_<tenant_id>_<random>
ComponentDescription
prefixYour app identifier (default: z0)
modelive or test
tenant_idThe tenant this key belongs to
randomCryptographically random suffix

Example: myapp_live_acme_corp_7f3a9b2c1d4e5f

function generateApiKey(prefix: string, mode: string, tenantId: string): string {
const random = crypto.randomUUID().replace(/-/g, '').substring(0, 16);
return `${prefix}_${mode}_${tenantId}_${random}`;
}

When using Workers for Platforms, verify keys at the dispatch worker level:

// Dispatch Worker
export default {
async fetch(request: Request, env: Env) {
const auth = await authenticate(request, env);
if (auth instanceof Response) return auth;
// Route to tenant's worker
const userWorker = env.DISPATCHER.get(auth.tenantId);
return userWorker.fetch(request);
}
}

For internal service-to-service calls, use a different authentication mechanism (e.g., signed tokens or mutual TLS) rather than API keys.