Express
WIPWriting the auth routes on Express 4 or 5 over auth.flows, guarding your own routes, and binding the actor
import { AuthError } from '@gentleduck/auth/core'
import {
applyIntents,
expressActorContext,
expressCaller,
expressCsrf,
toHeaders,
} from '@gentleduck/auth/server/express'
import { errorToHttp } from '@gentleduck/auth/server/generic'
import express from 'express'
const app = express()
app.set('trust proxy', 1)
app.use(express.json())
app.use('/auth', expressCsrf(auth))
app.post('/auth/signin', async (req, res) => {
const { providerId, input } = req.body ?? {}
if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
const headers = toHeaders(req.headers)
const { intents } = await auth.flows.signIn({
input: input ?? {},
providerId,
...expressCaller(req),
previousSid: auth.transport.extract({ headers }) ?? undefined,
})
applyIntents(intents, res)
})
app.post('/auth/signout', async (req, res) => {
const sid = auth.transport.extract({ headers: toHeaders(req.headers) })
applyIntents(sid ? (await auth.flows.signOut(sid)).intents : auth.transport.revoke(), res)
})
app.get('/auth/session', async (req, res) => {
const resolved = await auth.resolveSession({ headers: toHeaders(req.headers) }).orNull()
if (!resolved) return applyIntents([{ type: 'json', status: 200, body: { session: null, identity: null } }], res)
// `csrfHash` is server-side state; the browser holds the plaintext
const { csrfHash: _csrfHash, ...session } = resolved.session
applyIntents([{ type: 'json', status: 200, body: { session, identity: resolved.identity } }], res)
})
app.use((err, _req, res, _next) => {
const { status, body } = errorToHttp(err)
res.status(status).json(body)
})
- Express 5 hands an async throw to the error handler. On Express 4, catch it in each route and call
next(err). trust proxydecides which IPreq.ipanswers, and so what the session records. Set it to the number of proxies in front of you.applyIntents(intents, res)writes a flow's answer, cookies included, withCache-Control: no-store.- The provider routes are on the OAuth page. Mount the callback before
expressCsrf, and parse its form body withexpress.text({ type: 'application/x-www-form-urlencoded' }).
Your own routes
app.use('/api', expressCsrf(auth), expressActorContext(auth))
app.get('/api/me', async (req, res) => {
const resolved = await auth.resolveSession({ headers: toHeaders(req.headers) }).orNull()
if (!resolved) {
res.status(401).end()
return
}
res.json(resolved.identity)
})
expressCsrfanswers the 403 itself, so it does not depend on your error handler.expressActorContextpasses a refusal tonext(err). ItsAuthErrorcarriesstatusandtoJSON()for your error handler.