FabricFabricHarness
Operating

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 server
curl -H 'Authorization: Bearer changeme' http://localhost:9111/sessions

WebSocket 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 request
  • false — reject with 401
  • undefined — 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.

See also