Skip to main content

Elysia

WIP

Writing the auth routes on Elysia over auth.flows, giving sessions the client IP, and guarding your own routes

import { AuthError } from '@gentleduck/auth/core'
import { elysiaCaller, elysiaCsrf } from '@gentleduck/auth/server/elysia'
import { errorResponse, executeIntents, jsonResponse } from '@gentleduck/auth/server/generic'
import { Elysia } from 'elysia'

const app = new Elysia()
  .onError(({ error }) => errorResponse(error))
  .derive(({ server, request }) => ({ ip: server?.requestIP(request)?.address }))
  .onBeforeHandle(elysiaCsrf(auth))
  .post('/auth/signin', async (ctx) => {
    const { providerId, input } = ctx.body ?? {}
    if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
    const { intents } = await auth.flows.signIn({
      input: input ?? {},
      providerId,
      ...elysiaCaller(ctx),
      previousSid: auth.transport.extract(ctx.request) ?? undefined,
    })
    return executeIntents(intents)
  })
  .post('/auth/signout', async ({ request }) => {
    const sid = auth.transport.extract(request)
    return executeIntents(sid ? (await auth.flows.signOut(sid)).intents : auth.transport.revoke())
  })
  .get('/auth/session', async ({ request }) => {
    const resolved = await auth.resolveSession(request).orNull()
    if (!resolved) return jsonResponse(200, { session: null, identity: null })
    // `csrfHash` is server-side state; the browser holds the plaintext
    const { csrfHash: _csrfHash, ...session } = resolved.session
    return jsonResponse(200, { session, identity: resolved.identity })
  })
  • Elysia resolves no client IP. The derive gives elysiaCaller the socket's address, which the session records; behind a proxy, derive it from the header your proxy sets instead.
  • A hook covers only the routes after it, so onBeforeHandle(elysiaCsrf(auth)) guards everything below it.
  • The provider routes are on the OAuth page. Mount the callback above the guard, with { parse: 'none' } on its POST so Apple's form body reads as text.

Your own routes

import { elysiaWithActor } from '@gentleduck/auth/server/elysia'

app.group('/api', (api) =>
  api.onBeforeHandle(elysiaCsrf(auth)).get(
    '/me',
    elysiaWithActor(auth, async ({ request }) => {
      const resolved = await auth.resolveSession(request).orNull()
      if (!resolved) return Response.json({ error: 'unauthenticated' }, { status: 401 })
      return Response.json(resolved.identity)
    }),
  ),
)

Elysia composes no next, so the actor scope wraps each handler.