Skip to main content

Core overview

WIP

AuthEngine is the single faceted root. Sessions, identities, passwords, providers, MFA, flows, API keys, M2M, orgs, idempotency, hijack policy, anomaly - all on one instance.

What is AuthEngine?

AuthEngine is the runtime entry point for duck-auth. You instantiate it once at boot, wire it with the transport, stores, and providers you want, and hand the instance to your framework adapter.

import { AuthEngine } from '@gentleduck/auth/core'

export const auth = new AuthEngine({ /* config */ })

Every concern is exposed as a typed facet on the instance. There is no hidden global, no module-level state, no implicit singleton.

The 14 facets

FacetWhat it owns
sessionsCreate / rotate / revoke / resolve sessions. The fixation-defence rotation matrix (DESIGN section 37) routes every privilege change through rotateOrCreate().
identitiesCRUD + linking + merge + GDPR (soft-delete, restore, erase, exportAll, bulkCreate). Typed Profile generic.
passwordsHash / verify / needsRehash against the configured AuthHasher.IHasher. Auto-rehash on successful sign-in if hasher params drift.
providersRegistry and dispatcher for every sign-in method. auth.providers.use(...) registers; the flows facet dispatches.
mfaTOTP (RFC 6238), backup codes (constant-time compare), WebAuthn-MFA factor enrolment.
flowsHigh-level flows: signIn, signOut, signUp state machine, password reset, email change, impersonate.
apiKeysCreate (plaintext shown once), list, revoke, rotate, verify, requireScopes.
m2mOAuth2 client-credentials on top of apiKeys. Adds a sign-in path when registered.
orgsMulti-org membership with typed OrgMeta. null if stores.orgs is omitted.
pluginsPlugin registry - use(plugin), installed, facets, dispose.
operationsMaintenance mode + read-only mode toggles. Causes AUTH/MAINTENANCE / AUTH/READONLY_MODE.
idempotencyRequest-dedupe cache (24h TTL default). Keyed by header idempotency-key.
hijackIP/UA drift policy - ignore / rotate / mfa / revoke. Triggers lockout event.
anomalyDetector registry. Aggregates detector signals into allow / deny / step-up.

Constructor at a glance

import { AuthEngine, AuthInMemoryEvents, AuthScryptHasher } from '@gentleduck/auth/core'
import { CookieTransport } from '@gentleduck/auth/core/transport'
import { AuthMemoryAdapter } from '@gentleduck/auth/adapters/memory'
import { AuthMemoryLimiter } from '@gentleduck/auth/limiters/memory'

const adapter = new AuthMemoryAdapter()

export const auth = new AuthEngine({
  baseUrl: 'http://localhost:3000',
  transport: new CookieTransport({ secure: false }),
  stores: {
    identities: adapter.identities,
    sessions: adapter.sessions,
    credentials: adapter.credentials,
    // orgs is optional - pass adapter.orgs to enable the orgs facet
  },
  events: new AuthInMemoryEvents(),
  limiter: new AuthMemoryLimiter({ max: 5, windowMs: 60_000 }),
  passwords: { hasher: new AuthScryptHasher() },
})
Config keyPurpose
baseUrlIssuer for JWTs, OIDC discovery, signed redirect callbacks.
transportThe session carrier (CookieTransport / BearerTransport / JwtTransport / CompositeTransport).
storesidentities, sessions, credentials, optionally orgs. Implements the per-table store interfaces.
limiterRate-limiter. Use AuthMemoryLimiter in dev; Redis-backed in production. strict() rejects AuthNoopLimiter in production.
sessionPer-session TTLs: ttlMs (default 7d), absoluteTtlMs (default 30d), freshnessMs (default 5m).
identitiessoftDeleteGracePeriodMs (default 7d), profileMaxBytes (default 16 KiB).
passwordshasher plus optional minLength / maxLength / rejectCommon.
providersArray of AuthProvider.IProvider instances to register at boot.
eventsAuthEvents.IBus. Default is AuthInMemoryEvents - swap to AuthRedisEvents or a custom bus for fan-out.
mfaissuer, backupCodeCount, backupCodeLen.
apiKeysprefix (e.g. ak_live_), randomBytes.
hijackDetection sensitivity + reaction (ignore/rotate/mfa/revoke).
i18nLocale + message catalogs.

The request lifecycle

Loading diagram...

const ctx = await auth.resolveSession(req, {
  expectedTenantId: 'org-123',          // optional cross-tenant guard
  requestSnapshot: { ip, userAgent },   // optional, drives anomaly detectors
})

if (!ctx) {
  return new Response('Unauthorized', { status: 401 })
}

// ctx.session, ctx.identity, ctx.anomaly?  <- all typed

If anomaly detectors are wired and requestSnapshot is provided, ctx.anomaly.decision is one of allow / deny / step-up.

Strict mode

AuthEngine.strict() is a boot-time validator. Call it after construction to catch misconfigurations early.

auth.strict({ env: 'development' })  // warns only
auth.strict({ env: 'production' })   // throws AUTH/MISCONFIGURED

In production, strict rejects:

  • CookieTransport.secure: false
  • AuthMemoryAdapter (the in-memory dev store)
  • AuthMemoryLimiter (the in-memory dev limiter) or no limiter at all
  • No listeners attached to the lockout event
  • Insecure JWT keys (short HS256 secret, missing kid)
  • Compliance preset violations (HIPAA/FIPS require Argon2 + dataAtRest)
  • A registered provider with no callback URL when baseUrl is set

See Production hardening for the full list of gates.

Typed generics

AuthEngine<Profile, Tenant, OrgMeta> is parameterised by three shapes:

type Profile = { displayName: string; avatarUrl?: string }
type Tenant = { region: 'eu' | 'us'; tier: 'free' | 'pro' }
type OrgMeta = { plan: 'team' | 'enterprise' }

export const auth = new AuthEngine<Profile, Tenant, OrgMeta>({
  /* config */
})

The generics propagate everywhere: auth.identities.findById(id) returns Identity<Profile>, auth.orgs.list(identityId) returns Membership<OrgMeta>[], and provider hooks see Profile on every payload.

Where each facet lives