Skip to main content

Koa

WIP

Writing the auth routes on Koa with @koa/router over auth.flows, guarding your own routes, and binding the actor

import { AuthError } from '@gentleduck/auth/core'
import { errorToHttp, nodeHeadersToFetch } from '@gentleduck/auth/server/generic'
import { koaActorContext, koaApplyIntents, koaCaller, koaCsrf } from '@gentleduck/auth/server/koa'
import { bodyParser } from '@koa/bodyparser'
import Router from '@koa/router'
import Koa from 'koa'

const app = new Koa({ proxy: true })
const router = new Router()

router.use('/auth', koaCsrf(auth))

router.post('/auth/signin', async (ctx) => {
  const { providerId, input } = ctx.request.body ?? {}
  if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
  const headers = nodeHeadersToFetch(ctx.request.headers)
  const { intents } = await auth.flows.signIn({
    input: input ?? {},
    providerId,
    ...koaCaller(ctx),
    previousSid: auth.transport.extract({ headers }) ?? undefined,
  })
  await koaApplyIntents(intents, ctx)
})

router.post('/auth/signout', async (ctx) => {
  const sid = auth.transport.extract({ headers: nodeHeadersToFetch(ctx.request.headers) })
  await koaApplyIntents(sid ? (await auth.flows.signOut(sid)).intents : auth.transport.revoke(), ctx)
})

router.get('/auth/session', async (ctx) => {
  const resolved = await auth.resolveSession({ headers: nodeHeadersToFetch(ctx.request.headers) }).orNull()
  if (!resolved) return koaApplyIntents([{ type: 'json', status: 200, body: { session: null, identity: null } }], ctx)
  // `csrfHash` is server-side state; the browser holds the plaintext
  const { csrfHash: _csrfHash, ...session } = resolved.session
  await koaApplyIntents([{ type: 'json', status: 200, body: { session, identity: resolved.identity } }], ctx)
})

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    const { status, body } = errorToHttp(err)
    ctx.status = status
    ctx.set('cache-control', 'no-store')
    ctx.body = body
  }
})
app.use(bodyParser())
app.use(router.routes())
  • Koa answers a throw as plain text, so the first middleware answers it with errorToHttp instead: the AuthError's status and the { ok: false, error } body the other adapters send.
  • proxy: true makes ctx.request.ip read the forwarded address, and so what the session records. Leave it off unless a proxy you control sets that header.
  • The provider routes are on the OAuth page. Mount the callback before koaCsrf, and let the body parser read Apple's form post as text: bodyParser({ enableTypes: ['json', 'text'], extendTypes: { text: ['application/x-www-form-urlencoded'] } }).

Your own routes

router.use('/api', koaCsrf(auth), koaActorContext(auth))

koaActorContext throws a refusal, which the error middleware answers at the error's status.