Skip to main content

Introduction

WIP

duck-auth is framework-agnostic authentication for TypeScript, built around one engine and pluggable stores, transports and providers

duck-auth (@gentleduck/auth) proves who a caller is. It signs people in with passwords, magic links, OAuth, passkeys and SAML, lifts sessions to a second factor with TOTP or WebAuthn, and signs machines in with API keys and client credentials. It runs in your process, stores its rows in your database, and mounts on the framework you already use.

What duck-auth is

An authentication engine. You build one AuthEngine at boot from a store, a transport and a list of providers, and then call it:

const resolved = await auth.resolveSession({ headers }).orNull()
if (!resolved?.identity) throw new AuthError('AUTH_UNAUTHENTICATED')

duck-auth is not authorization. It answers "who is this?", never "may they do this?". Pair it with duck-iam for that half; the iam bridge guide wires the two together.

duck-auth sends no mail. Every token it mints (a magic link, a reset link, a verification link) goes to one deliver function you write, so the mailer is whatever your app already uses. See Delivery.

Feature map

AreaWhat ships
EnginecreateAuth() builds an AuthEngine with sessions, identities, flows, providers, hijack, anomaly and plugins facets, plus passwords, mfa, passkeys, apiKeys and orgs once their provider or store is registered.
ProvidersPasswords (scrypt or argon2id), magic link, TOTP and backup codes, WebAuthn MFA, passkeys, API keys, SAML, and OAuth for Google, GitHub, Microsoft, Apple, LinkedIn and Discord.
TransportsCookie, bearer and JWT, a composite of them, and DPoP proof-of-possession.
StoresIn-memory for tests, Drizzle for Postgres, MySQL and SQLite, and Redis or Valkey for sessions, events and single-use tokens.
LimitersIn-memory, Redis and Valkey; every flow spends against one.
ServersGlue for Express, Hono, Next.js, Fastify, Koa, Elysia, NestJS, gRPC and any Fetch runtime: CSRF, the caller, writing a flow's intents. The routes are yours, over auth.flows.
HardeningCSRF, strict() production checks, compliance presets, hijack and anomaly detection, captcha verifiers, field encryption at rest, audit stamping.

Architecture

Loading diagram...

Your route turns a request into a call on auth.flows, and the framework adapter turns the answer back into a response. The answer is a list of intents (set this cookie, redirect there, send this JSON), so the engine never touches a framework's response object.

Quick example

import { MemoryAdapter } from '@gentleduck/auth/adapters/memory'
import { createAuth } from '@gentleduck/auth/core'
import { cookieTransport } from '@gentleduck/auth/core/transport'
import { passwords } from '@gentleduck/auth/providers/passwords'

const auth = createAuth({
  baseUrl: 'http://localhost:3000',
  stores: new MemoryAdapter(),
  transport: cookieTransport({ name: 'sid', secure: false }),
  providers: [passwords()],
})

const ada = await auth.identities.create({ profile: { email: 'ada@example.com', name: 'Ada', username: 'ada' } })
await auth.passwords.set(ada.id, 'correct-horse-battery', auth.cfg.stores.credentials)

const { sid } = await auth.flows.signIn({
  providerId: 'password',
  input: { email: 'ada@example.com', password: 'correct-horse-battery' },
})

const resolved = await auth.resolveSession({ headers: new Headers({ cookie: `sid=${sid}` }) }).orNull()
resolved?.identity?.profile.email // 'ada@example.com'

Next steps

  • Installation: install the package and the peers your stack needs.
  • Quick start: a password sign-in behind Express in one file.
  • Course: build a full app across eight chapters.
  • Core: the engine, its facets and its config.
  • Production hardening: what strict() refuses in production, and the order to build the engine in.