Skip to main content

Hono

WIP

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

import { AuthError } from '@gentleduck/auth/core'
import { errorResponse, executeIntents, jsonResponse, readBodyJson } from '@gentleduck/auth/server/generic'
import { honoCaller, honoCsrf } from '@gentleduck/auth/server/hono'
import { type Context, Hono } from 'hono'
import { getConnInfo } from 'hono/bun'

const app = new Hono()
const caller = (c: Context) => honoCaller({ ip: getConnInfo(c).remote.address, req: c.req })

app.onError((err) => errorResponse(err))
app.use('/auth/*', honoCsrf(auth))

app.post('/auth/signin', async (c) => {
  const body = await readBodyJson(c.req.raw)
  if (typeof body !== 'object' || body === null) throw new AuthError('AUTH_INVALID_PARAMETERS')
  const providerId: unknown = Reflect.get(body, 'providerId')
  if (typeof providerId !== 'string') throw new AuthError('AUTH_INVALID_PARAMETERS')
  const { intents } = await auth.flows.signIn({
    input: Reflect.get(body, 'input') ?? {},
    providerId,
    ...caller(c),
    previousSid: auth.transport.extract(c.req.raw) ?? undefined,
  })
  return executeIntents(intents)
})

app.post('/auth/signout', async (c) => {
  const sid = auth.transport.extract(c.req.raw)
  return executeIntents(sid ? (await auth.flows.signOut(sid)).intents : auth.transport.revoke())
})

app.get('/auth/session', async (c) => {
  const resolved = await auth.resolveSession(c.req.raw).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 })
})

export default app
  • Hono resolves no client IP, so honoCaller takes one. Use the getConnInfo for your runtime, or read the header your proxy sets if you trust it. Without it, sessions record none.
  • executeIntents answers a flow's intents as a Response, and jsonResponse a JSON body, both with Cache-Control: no-store.
  • The provider routes are on the OAuth page. Register the callback before the CSRF middleware: Hono runs handlers in registration order, so a route that answers first never reaches it.
  • CORS is yours to mount, with hono/cors.

Your own routes

import { honoActorContext } from '@gentleduck/auth/server/hono'

app.use('/api/*', honoCsrf(auth), honoActorContext(auth))

app.get('/api/me', async (c) => {
  const resolved = await auth.resolveSession({ headers: c.req.raw.headers }).orNull()
  if (!resolved) return c.json({ error: 'unauthenticated' }, 401)
  return c.json(resolved.identity)
})

honoActorContext answers a refusal itself, since Hono's default error handler turns any throw into a 500.