CookieTransport
WIPSession id in an HttpOnly, __Host-prefixed, SameSite=Lax cookie. The safe first-party default.
When to use
For any first-party web app served from a single origin (your auth
endpoint and your app share a hostname). CookieTransport ships every
defence-in-depth bit out of the box:
__Host-prefix (nodomain, nopathother than/,securemandatory).HttpOnly(JavaScript cannot read or modify the cookie).SameSite=Lax(no cross-site POST gives the cookie up).- Constant-time session lookup with a hashed-at-rest session id.
- Double-submit CSRF defence wired through
authCsrfGuard.
Constructor
import { CookieTransport } from '@gentleduck/auth/core/transport'
new CookieTransport({
name: '__Host-duck-sid', // default; falls back to 'duck-sid' if `domain` is set
secure: true, // default; required for __Host- prefix
sameSite: 'lax', // default; or 'strict' / 'none'
path: '/', // default; required for __Host- prefix
domain: undefined, // omit for __Host- prefix
maxAgeSec: 7 * 24 * 60 * 60, // default 7 days
})
In development, secure: false is allowed - but only strict({ env: 'development' })
will accept it. Production calls to strict({ env: 'production' }) throw
AUTH/MISCONFIGURED if secure is not true.
Wire format
The cookie value is the session id, not the session itself.
duck-auth always stores the canonical session in the configured
sessions store; the cookie is just the key.
Set-Cookie: __Host-duck-sid=01HJX7B2T9...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=604800
The id is a 256-bit random value (base64url-encoded), generated with
crypto.randomBytes(32). At rest, the store records a hash of the
id - even a database leak does not yield usable session tokens.
CSRF defence
Cookies ride along on cross-site POST/PUT/DELETE, so every state-mutating
endpoint must call authCsrfGuard:
import { authCsrfGuard, authIssueCsrfToken } from '@gentleduck/auth/core'
// Issue: on session start, set a non-HttpOnly companion cookie.
const intents = await auth.flows.signIn({ providerId: 'password', input })
intents.push({ kind: 'setCookie', cookie: {
name: '__Host-duck-csrf',
value: authIssueCsrfToken(),
path: '/',
secure: true,
sameSite: 'lax',
httpOnly: false, // <- intentionally readable by the client SPA
}})
// Verify: on every state-mutating request.
await authCsrfGuard(auth, req) // throws AUTH/CSRF on mismatch
The CSRF token is double-submit: the client reads __Host-duck-csrf
from the cookie and echoes it back as X-CSRF-Token on every mutating
request. authCsrfGuard constant-time-compares them.
GET/HEAD/OPTIONS are exempt, as are Bearer/JWT routes (no cookies, no CSRF).
Sign-out
import { authMountSignOut } from '@gentleduck/auth/server/express'
app.post('/auth/signout', authMountSignOut(auth))
The transport emits a Set-Cookie with Max-Age=0 to clear the cookie
and revokes the underlying session row in the store. Even if the cookie
is replayed, the session id no longer resolves to anything.
Custom name + domain (sub-domain cookies)
If you serve auth from auth.example.com and the app from app.example.com,
the __Host- prefix won't work (it forbids domain). Drop the prefix
and set domain explicitly:
new CookieTransport({
name: 'duck-sid',
secure: true,
sameSite: 'lax',
path: '/',
domain: '.example.com', // shared across subdomains
})
Be aware: dropping __Host- weakens some defences (cookie can be
overwritten by a less-secure subdomain). Prefer same-origin if you can.