Documentation
Authentication
Kynetra FX ships a first-class auth layer via @kynetra/fx-auth. It provides composable strategies (JWT, API key, cookie session), a unified Principal identity model, and guard middleware that integrates cleanly with RBAC and the SaaS kernel.
How authentication works#
Authentication in Kynetra FX is handled by two middleware primitives: auth() and requireAuth(). Both accept an ordered list of strategies. On each request, strategies are tried in sequence — the first one that resolves a valid identity wins. The resolved Principal is stored on the context and read later by getPrincipal(ctx).
auth(strategies[], { required? })— runs strategies; ifrequired: trueand none resolve, returns401 FX_UNAUTHENTICATED. Defaults torequired: false(anonymous pass-through).requireAuth(strategies[])— shorthand forauth(strategies, { required: true }). Always enforces authentication.
import { createFX } from '@kynetra/fx'import { auth, requireAuth, jwtAuth, getPrincipal } from '@kynetra/fx-auth' const app = createFX() // Optional auth — anonymous callers are allowed throughapp.use(auth([jwtAuth({ secret: env.JWT_SECRET })])) // Protected route — 401 if no valid tokenapp.get('/me', requireAuth([jwtAuth({ secret: env.JWT_SECRET })]), (ctx) => { const principal = getPrincipal(ctx) return ctx.json({ id: principal.id, roles: principal.roles })})The Principal model#
Every authenticated identity is normalised into a Principal object. This uniform shape means your route handlers and RBAC guards work the same way regardless of whether the caller authenticated with a JWT, an API key, or a session cookie.
interface Principal { id: string // stable identifier for this identity type: 'user' | 'apikey' | 'session' | 'anonymous' roles: string[] // e.g. ['admin', 'member'] scopes: string[] // e.g. ['issues:read', 'issues:write'] tenantId?: string // set when using tenant middleware claims?: Record<string, unknown> // raw JWT/session payload}Unauthenticated requests (when required is false) receive the built-in ANONYMOUS principal: { id: "anonymous", type: "anonymous", roles: [], scopes: [] }.
Reading the principal#
Call getPrincipal(ctx) anywhere in a handler or downstream middleware. It always returns a Principal — the anonymous sentinel when no strategy resolved.
import { getPrincipal } from '@kynetra/fx-auth' app.get('/profile', (ctx) => { const principal = getPrincipal(ctx) if (principal.type === 'anonymous') { return ctx.json({ guest: true }) } return ctx.json({ userId: principal.id, tenantId: principal.tenantId })})Available strategies#
All strategies are imported from @kynetra/fx-auth and passed as an array to auth() or requireAuth().
jwtAuth({ secret, header?, scheme?, now?, toPrincipal? })— HS256 JWT via Web Crypto. Reads theAuthorizationheader by default.apiKeyAuth({ header?, lookup(key, ctx) })— resolves an opaque API key via your lookup function.sessionAuth({ secret, cookie?, now? })— signed cookie sessions; usesignSession()to issue them.
Tip
Combining strategies#
Pass multiple strategies to support several authentication methods on the same route. A common pattern for SaaS APIs: accept either a JWT (for first-party clients) or an API key (for integrations).
import { requireAuth, jwtAuth, apiKeyAuth } from '@kynetra/fx-auth'import { kernel } from './kernel' const strategies = [ jwtAuth({ secret: env.JWT_SECRET }), apiKeyAuth({ lookup: async (key, ctx) => { const record = await kernel.apiKeys.list({ key }) if (!record[0]) return null return { id: record[0].id, type: 'apikey' as const, roles: record[0].roles ?? [], scopes: record[0].scopes ?? [], tenantId: record[0].tenantId, } }, }),] app.use('/api/*', requireAuth(strategies))Security profiles#
securityProfile() returns a preset configuration object covering auth, cookies, CSRF, and CORS — tuned for three common SaaS deployment patterns.
'api'— stateless bearer-token API; no cookies, no CSRF.'browser'— cookie sessions with CSRF protection; suitable for server-rendered apps.'saas'— hybrid: JWT for the API layer, session cookies for the dashboard.
import { securityProfile } from '@kynetra/fx-auth' const profile = securityProfile('saas')// profile.requireAuth → the auth middleware preset// profile.cookies → recommended cookie settings// profile.csrf → CSRF configuration// profile.cors → CORS configurationError codes#
When a required strategy fails, the framework returns a structured JSON error. Use these codes in your client to branch on auth failures without parsing strings.
401 FX_UNAUTHENTICATED— no valid identity resolved and auth was required.403 FX_FORBIDDEN— authenticated but lacks the required role or permission (see RBAC).
End-to-end: auth → tenant → RBAC#
The following example wires together authentication, tenant resolution, and permission guards into a single middleware stack — the foundation of a multi-tenant SaaS API.
import { createFX } from '@kynetra/fx'import { requireAuth, jwtAuth, getPrincipal } from '@kynetra/fx-auth'import { tenant, getTenantId } from '@kynetra/fx-saas'import { createRbac, defineRoles } from '@kynetra/fx-rbac' const roles = defineRoles({ admin: ['*'], member: ['issues:read', 'issues:create'], viewer: ['issues:read'],}) const rbac = createRbac(roles) const app = createFX({ runtime: 'cloudflare' }) // 1. Authenticateapp.use(requireAuth([jwtAuth({ secret: env.JWT_SECRET })])) // 2. Resolve tenant from x-tenant-id headerapp.use(tenant({ required: true })) // 3. Guard specific routes with permission checksapp.get('/issues', rbac.requirePermission('issues:read'), async (ctx) => { const principal = getPrincipal(ctx) const tenantId = getTenantId(ctx) // principal.tenantId === tenantId return ctx.json({ tenantId, issues: [] })}) app.post('/issues', rbac.requirePermission('issues:create'), async (ctx) => { const body = await ctx.jsonBody() return ctx.json({ created: true }, { status: 201 })})Note