Server adapters
WIPThe auth routes your app writes over auth.flows, what each must check, and the per-framework glue for them, such as the CSRF guard, the caller fingerprint, the intents writer and the actor scope
duck-auth ships no routes and no client. Your app writes its own routes over auth.flows.* and calls them with its own fetch. Each adapter carries only the glue those routes need for one framework. None imports its framework: each is typed against the few methods it calls.
The routes you write
| Route | Calls |
|---|---|
POST /auth/signin | auth.flows.signIn({ providerId, input, previousSid, ...caller }) |
POST /auth/signout | auth.flows.signOut(sid), or auth.transport.revoke() when the request holds no session |
GET /auth/session | auth.resolveSession({ headers }).orNull() |
POST /auth/providers/:id/begin | auth.flows.beginProvider(id, input): the OAuth redirect, the magic-link email, the passkey challenge |
GET and POST /auth/providers/:id/callback | auth.flows.signIn with the IdP's code and state |
The paths are yours to choose. Write only what you use; a password-only app needs the first three. examples/duck-auth has the full set for every framework below.
Adapters
| Framework | Import | CSRF | Caller | Writes intents | Actor scope |
|---|---|---|---|---|---|
| Express | server/express | expressCsrf | expressCaller | applyIntents | expressActorContext |
| Fastify | server/fastify | fastifyCsrf | fastifyCaller | executeIntents | fastifyWithActor |
| Hono | server/hono | honoCsrf | honoCaller | executeIntents | honoActorContext |
| Koa | server/koa | koaCsrf | koaCaller | koaApplyIntents | koaActorContext |
| Elysia | server/elysia | elysiaCsrf | elysiaCaller | executeIntents | elysiaWithActor |
| Next.js | server/next | withNextCsrf | nextCaller | executeIntents | nextWithActor |
| NestJS | server/nestjs | makeCsrfGuard, makeGuard | nestCaller | applyIntents | nestActorContext |
| gRPC | server/grpc | withGrpc | |||
| Any Fetch runtime | server/generic | csrfGuard from core | callerContext | executeIntents |
Every import is under @gentleduck/auth/. executeIntents is in server/generic, and NestJS on platform-express takes Express's applyIntents.
What each route must do
- Guard CSRF on sign-in, sign-out, begin and every other mutation. The guard answers 403
AUTH_CSRF: a cross-siteSec-Fetch-Siteis refused, and once a session exists, thex-csrf-tokenheader must match its CSRF cookie. A bearer request with no cookie skips the token. - Pass the caller from the adapter's caller helper, so the session records the User-Agent and the IP the framework resolved, never a forwarded header. Set your framework's proxy trust so that IP is the client's.
- Pass
previousSid, the session the request came in with, so signing in again revokes it instead of leaving two. - Strip
csrfHashfrom any session you answer with. It is server-side state; the browser holds the plaintext. - Answer
Cache-Control: no-store. The intents writers andjsonResponseset it. - Map a throw with
errorToHttp, which answers anAuthErroras{ ok: false, error: { code, status, detail? } }at its status, and anything else as a 500 with the cause logged and withheld. - Answer
beginto a script as{ url }when it redirects, checked withisSafeRedirectUrl. A script cannot follow a redirect to the IdP.
The OAuth callback is the exception to CSRF: Apple posts it from its own page, and the signed state bound to the pre-auth cookie is the proof instead. Keep it to oauth providers, so it signs nobody in any other way. See OAuth.
Your own routes
For every other route:
- Read the session with
auth.resolveSession({ headers })..orNull()answersnullwhen there is none, and still throws when the store is down. - Guard it against CSRF with the adapter's CSRF helper, on every state-changing route a cookie session reaches.
- Bind the actor with the adapter's actor scope, so writes downstream record who made them in
created_byandupdated_by. Give itgetCaller, and it also runs hijack detection and the anomaly detectors on every request.
The actor scope authorises nothing: it binds nobody for a request with no session, and refusing that is your guard's job.