Documentation
API Keys & Sessions
Beyond JWT, Kynetra FX provides apiKeyAuth for opaque API keys resolved via your own lookup logic, and sessionAuth paired with signSession for HMAC-signed cookie sessions. Both integrate with the same auth() middleware.
API key authentication#
apiKeyAuth({ lookup }) reads an opaque key from a request header and delegates resolution to your lookup function. The function receives the raw key string and the request context, and must return a Principal or null (meaning the key is unknown or revoked).
import { requireAuth, apiKeyAuth } from '@kynetra/fx-auth'import { kernel } from './kernel' const strategy = apiKeyAuth({ header: 'x-api-key', // default: 'x-api-key' lookup: async (key, ctx) => { const [record] = await kernel.apiKeys.list({ key }) if (!record) return null // unknown key return { id: record.id, type: 'apikey' as const, roles: record.roles ?? [], scopes: record.scopes ?? [], tenantId: record.tenantId, } },}) app.use('/api/*', requireAuth([strategy]))apiKeyAuth options
| Name | Type | Description |
|---|---|---|
| header | string | Request header that carries the API key. Defaults to "x-api-key". |
| lookup | (key: string, ctx: FXContext) => Promise<Principal | null> | Required. Called with the raw key value. Return a Principal to authenticate, or null to reject. |
Tip
Issuing API keys
Kynetra FX does not generate API key strings — that is your responsibility. A common pattern: generate a cryptographically random token, store its hash in the SaaS kernel apiKeys repository, and return the plaintext once to the user.
import { kernel } from './kernel' app.post('/api-keys', async (ctx) => { const principal = getPrincipal(ctx) // Generate a random key const raw = Array.from(crypto.getRandomValues(new Uint8Array(32))) .map((b) => b.toString(16).padStart(2, '0')) .join('') // Hash before storing const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw)) const hash = Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, '0')) .join('') await kernel.apiKeys.create({ id: crypto.randomUUID(), key: hash, tenantId: principal.tenantId ?? '', roles: ['member'], scopes: ['api:read'], createdAt: new Date().toISOString(), }) // Return plaintext ONCE — never stored return ctx.json({ key: raw }, { status: 201 })})Session authentication#
sessionAuth({ secret, cookie?, now? }) authenticates requests using a signed session cookie. The cookie value is a compact HMAC-signed string produced by signSession. This is suitable for browser-based apps where you control the session lifecycle server-side.
Signing sessions
Call signSession(claims, secret, opts?) to produce a signed session string, then set it as a cookie. It accepts the same optional timing parameters as signJwt.
import { signSession } from '@kynetra/fx-auth' app.post('/login', async (ctx) => { const { email, password } = await ctx.jsonBody<{ email: string; password: string }>() const user = await authenticateUser(email, password) const session = await signSession( { sub: user.id, tenantId: user.tenantId, roles: user.roles }, env.SESSION_SECRET, { expiresInSec: 60 * 60 * 8 } // 8 hours ) ctx.setCookie('session', session, { httpOnly: true, secure: true, sameSite: 'Lax', path: '/', maxAge: 60 * 60 * 8, }) return ctx.json({ ok: true })})Verifying sessions
Use sessionAuth as a strategy in the normal auth() flow. It reads the named cookie, verifies the HMAC signature, checks expiry, and resolves a Principal.
import { requireAuth, sessionAuth } from '@kynetra/fx-auth' app.use( requireAuth([ sessionAuth({ secret: env.SESSION_SECRET, cookie: 'session', // default: 'session' }), ]))sessionAuth options
| Name | Type | Description |
|---|---|---|
| secret | string | HMAC secret for signing and verifying the session cookie value. |
| cookie | string | Cookie name to read. Defaults to "session". |
| now | () => number | Override the current time for expiry validation. Useful in tests. |
Combining strategies#
Pass multiple strategies to auth() or requireAuth() to support several authentication methods on the same route. Strategies are tried in order — the first one that successfully resolves a Principal wins.
import { requireAuth, jwtAuth, apiKeyAuth, sessionAuth } from '@kynetra/fx-auth'import { kernel } from './kernel' // Try JWT (fast, no DB hit), then API key (DB lookup), then session cookieconst strategies = [ jwtAuth({ secret: env.JWT_SECRET }), apiKeyAuth({ lookup: async (key, _ctx) => { const [record] = await kernel.apiKeys.list({ key }) if (!record) return null return { id: record.id, type: 'apikey' as const, roles: [], scopes: [] } }, }), sessionAuth({ secret: env.SESSION_SECRET }),] // All authenticated routesapp.use('/app/*', requireAuth(strategies)) // Some routes allow anonymous access (e.g. public content)app.use('/public/*', auth(strategies)) // required: falseNote
auth() function from @kynetra/fx-auth accepts a second argument { required?: boolean }. Omitting it or passing required: false allows anonymous callers through. Use requireAuth() as a shorthand when auth is always mandatory.Session logout#
To log out a session user, clear the cookie by setting it with an expired max-age. There is no server-side session store to invalidate — the HMAC signature and expiry in the cookie are the only source of truth.
app.post('/logout', (ctx) => { ctx.setCookie('session', '', { httpOnly: true, secure: true, sameSite: 'Lax', path: '/', maxAge: 0, // immediately expires }) return ctx.json({ ok: true })})Tip
sessions repository and check validity in the apiKeyAuth lookup. Cookie sessions alone do not support revocation without a database check.