Auth
Bearer tokens, cookie-based session auth, and SSO terminator integration for fh server.
fh server ships a simple Bearer-token auth surface for solo deployments. For SaaS embedders, the extractAuthToken option lets you plug in cookies, JWTs, or headers set by an SSO terminator.
Default: Bearer token
FABRIC_HARNESS_API_TOKEN=changeme fh servercurl -H 'Authorization: Bearer changeme' http://localhost:9111/sessionsWebSocket upgrades use the same token via ?token= query param (browsers can't set headers on WebSocket constructor):
new WebSocket('wss://app.example.com/sessions/abc/ws?token=changeme');Custom resolver: extractAuthToken
When you have an existing identity layer (cookies, JWT, SSO terminator), pass a custom resolver:
import { startDevServer, parseCookie } from '@fabric-harness/node';
await startDevServer({
extractAuthToken: (req) => {
const cookie = parseCookie(req, 'session');
if (!cookie) return undefined; // fall through to bearer check
return verifySessionCookie(cookie); // your validator returns true/false
},
});The resolver returns:
true— authorize the requestfalse— reject with 401undefined— fall through to the built-in bearer-token check
Same hook works for HTTP requests AND WebSocket upgrades.
Recipe: SSO terminator (Cloudflare Access, IAP, Cognito)
When fabric-harness runs behind an SSO terminator, the terminator validates the user and forwards a verified header. Trust the header inside the resolver:
await startDevServer({
extractAuthToken: (req) => {
const userHeader = req.headers['cf-access-authenticated-user-email'];
if (typeof userHeader === 'string' && userHeader.length > 0) return true;
return undefined;
},
});Make sure the terminator strips the header from inbound requests that didn't pass through it — otherwise clients can spoof.
Recipe: JWT in Authorization header
import { jwtVerify } from 'jose';
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
await startDevServer({
extractAuthToken: async (req) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) return undefined;
try {
await jwtVerify(header.slice(7), SECRET);
return true;
} catch {
return false;
}
},
});Async resolvers are supported (v1.11+). Returning a Promise<boolean | undefined> works the same as the sync forms — true authorizes, false rejects, undefined falls through. JWT verification, remote token introspection, and any auth gate that needs a network call slot in directly:
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://my-idp.example.com/.well-known/jwks.json'));
await startDevServer({
extractAuthToken: async (req) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) return undefined;
try {
await jwtVerify(header.slice(7), JWKS, { issuer: 'https://my-idp.example.com', audience: 'fabric-harness' });
return true;
} catch {
return false;
}
},
});The same hook fires on WebSocket upgrades (chat WS + voice WS) — no separate auth path.
Recipe: per-tenant cookies
Combine with X-Fabric-Tenant header to scope cookie validation per tenant:
extractAuthToken: (req) => {
const tenant = req.headers['x-fabric-tenant'];
const cookie = parseCookie(req, `session_${tenant}`);
return cookie ? verifyForTenant(cookie, tenant) : undefined;
},WebSocket cookies (browsers)
Same-origin WebSocket upgrades carry browser cookies automatically — extractAuthToken reads them just like HTTP. Cross-origin? Sec-WebSocket-Protocol workarounds exist but are clunky; recommend bearer-via-query-param (?token=...) for cross-origin WS.