Kynetra FX

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; if required: true and none resolve, returns 401 FX_UNAUTHENTICATED. Defaults to required: false (anonymous pass-through).
  • requireAuth(strategies[]) — shorthand for auth(strategies, { required: true }). Always enforces authentication.
src/app.ts
import { createFX } from '@kynetra/fx'
import { auth, requireAuth, jwtAuth, getPrincipal } from '@kynetra/fx-auth'
 
const app = createFX()
 
// Optional auth — anonymous callers are allowed through
app.use(auth([jwtAuth({ secret: env.JWT_SECRET })]))
 
// Protected route — 401 if no valid token
app.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.

types
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.

src/handlers/me.ts
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().

Tip

Strategies are tried in the order you provide them. Put the cheapest or most common strategy first so the fast path wins without trying the others.

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).

src/app.ts
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.
src/app.ts
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 configuration

Error 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.

src/app.ts
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. Authenticate
app.use(requireAuth([jwtAuth({ secret: env.JWT_SECRET })]))
 
// 2. Resolve tenant from x-tenant-id header
app.use(tenant({ required: true }))
 
// 3. Guard specific routes with permission checks
app.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

See JWT, API Keys & Sessions, RBAC, and Tenancy for deep-dives into each layer.
Authentication · Kynetra FX