Skip to main content

Fastify

WIP

Writing the auth routes on Fastify over auth.flows, guarding them with a preHandler, and binding the actor per handler

import { AuthError } from '@gentleduck/auth/core'
import { fastifyCaller, fastifyCsrf } from '@gentleduck/auth/server/fastify'
import { errorToHttp, executeIntents, jsonResponse, nodeHeadersToFetch } from '@gentleduck/auth/server/generic'
import Fastify from 'fastify'

const app = Fastify({ trustProxy: 1 })

app.setErrorHandler((err, _req, reply) => {
  const { status, body } = errorToHttp(err)
  reply.status(status).header('cache-control', 'no-store').send(body)
})

app.register(async (own) => {
  own.addHook('preHandler', fastifyCsrf(auth))

  own.post<{ Body: { providerId?: unknown; input?: unknown } }>('/auth/signin', async (req, reply) => {
    const { providerId, input } = req.body ?? {}
    if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
    const headers = nodeHeadersToFetch(req.headers)
    const { intents } = await auth.flows.signIn({
      input: input ?? {},
      providerId,
      ...fastifyCaller(req),
      previousSid: auth.transport.extract({ headers }) ?? undefined,
    })
    return reply.send(executeIntents(intents))
  })

  own.post('/auth/signout', async (req, reply) => {
    const sid = auth.transport.extract({ headers: nodeHeadersToFetch(req.headers) })
    return reply.send(executeIntents(sid ? (await auth.flows.signOut(sid)).intents : auth.transport.revoke()))
  })

  own.get('/auth/session', async (req, reply) => {
    const resolved = await auth.resolveSession({ headers: nodeHeadersToFetch(req.headers) }).orNull()
    if (!resolved) return reply.send(jsonResponse(200, { session: null, identity: null }))
    // `csrfHash` is server-side state; the browser holds the plaintext
    const { csrfHash: _csrfHash, ...session } = resolved.session
    return reply.send(jsonResponse(200, { session, identity: resolved.identity }))
  })
})
  • Fastify sends a Web Response as it is, so executeIntents and jsonResponse serve as the intents writer.
  • trustProxy decides which IP req.ip answers, and so what the session records. Set it to the proxies in front of you.
  • The provider routes are on the OAuth page. Register the callback outside the guarded plugin. Apple posts it as a form, which Fastify refuses with a 415 unless a parser for application/x-www-form-urlencoded is registered.

Your own routes

import { fastifyWithActor } from '@gentleduck/auth/server/fastify'

app.register(async (api) => {
  api.addHook('preHandler', fastifyCsrf(auth))

  api.get(
    '/api/me',
    fastifyWithActor(auth, async (req, reply) => {
      const resolved = await auth.resolveSession({ headers: nodeHeadersToFetch(req.headers) }).orNull()
      if (!resolved) return reply.status(401).send({ error: 'unauthenticated' })
      return resolved.identity
    }),
  )
})

Fastify composes no next, so the actor scope wraps each handler instead of sitting in front of them.