Skip to content

Authentication

What if I told you… we do not need middlewares when we have dependency inversion?

Here is how it will work:

ts
type Authenticate = (headers: Headers) => Promise<Account>
// The trick is to throw a Response 401 if unauthenticated 😉
// Because you know, `throw` was actually made to interrupt a flow…

Implementing the authenticate factory

I will voluntarily use an authenticate factory function to emulate that we inject a persistence layer.

tsx
// src/server-first/5-authentication/authenticate.tsx

/* @jsxImportSource hono/jsx */
import * as x from 'unhoax'
import { respondWith } from '../definition/response'

export type Authenticate<Name> = (headers: Headers) => Promise<{ name: Name }>

export function makeAuthenticate<Name>(
  schema: x.Schema<Name>,
): Authenticate<Name> {
  return async function authenticate(headers) {
    const name = schema.parse(headers.get('Authorization'))
    if (name.success) return { name: name.value }
    throw respondWith
      .status(401)
      .html(<div style="color: red">We don’t know you…</div>)
  }
}

Updating the greet handler to use authenticate

ts
// src/server-first/5-authentication/greet-handler.tsx

/** @jsxImportSource hono/jsx */
import { delay } from '@/utils/delay'
import * as x from 'unhoax'
import { respondWith } from '../definition/response'
import { HandlerBuilder } from './handler-builder'
import { Authenticate } from './authenticate'
import { respondWith } from '../definition/response'

export const greetHandler = HandlerBuilder.post('/hello') 
  .body({ name: x.literal('John', 'Michelle') }, () => { 
    return respondWith 
      .status(400) 
      .html(<div style="color: red">Name must be “John” or “Michelle”</div>) 
  }) 
  .handleWith(async ({ body }) => { 
type Ports = { 
  authenticate: Authenticate<'John' | 'Michelle'> 
} 

export const makeGreetHandler = (ports: Ports) => { 
  return HandlerBuilder.get('/hello').handleWith(async ({ headers }) => { 
    // will throw if unauthenticated
    const { name } = await ports.authenticate(headers) 

    await delay(150) // mimic DB access.

    return respondWith.html(<div style="color: blue">Hello, {body.name}</div>) 
    return respondWith.html(<div style="color: blue">Hello, {name}</div>) 
  })
} 

Unit testing the greet handler

Spec file
ts
// src/server-first/5-authentication/greet-handler.spec.ts

import { describe, expect, it } from 'vitest'
import { makeGreetHandler } from './greet-handler'
import * as x from 'unhoax'
import { makeAuthenticate } from './authenticate'

describe('greetHandler – with authentication', () => {
  const authenticate = makeAuthenticate(x.literal('John', 'Michelle'))
  const greetHandler = makeGreetHandler({ authenticate })

  it('fails with 400 when no authorization provided', async () => {
    const result = await greetHandler
      .handle({
        params: {},
        body: undefined,
        query: {},
        headers: new Headers(),
      })
      .catch((err) => err)

    expect(result.status).toBe(401)
    expect(await result.text()).toBe(
      '<div style="color: red">We don’t know you…</div>',
    )
  })

  it('fails with 400 when authorization is incorrect', async () => {
    const result = await greetHandler
      .handle({
        params: {},
        body: undefined,
        query: {},
        headers: new Headers({ authorization: 'Jack' }),
      })
      .catch((err) => err)

    expect(result.status).toBe(401)
    expect(await result.text()).toBe(
      '<div style="color: red">We don’t know you…</div>',
    )
  })

  it('responds with 200 & a blue div', async () => {
    const result = await greetHandler.handle({
      params: {},
      body: undefined,
      query: {},
      headers: new Headers({ authorization: 'John' }),
    })
    expect(result.status).toBe(200)
    expect(await result.text()).toBe(
      '<div style="color: blue">Hello, John</div>',
    )
  })
})
sh
 server-first/5-authentication/greet-handler.spec.ts (3)
 greetHandler with authentication (3)
 fails with 400 when no authorization provided
 fails with 400 when authorization is incorrect
 responds with 200 & a blue div

Update the server

ts
// src/server-first/5-authentication/server.ts

import { greetHandler } from './greet-handler'
import * as x from 'unhoax'
import { makeAuthenticate } from './authenticate'
import { makeGreetHandler } from './greet-handler'
import { createH3NodeServer } from './h3-adapter'

async function createServer() {
  const port = 6600
  const authenticate = makeAuthenticate(x.literal('John', 'Michelle')) 

  await createH3NodeServer({
    port,
    handlers: [greetHandler], 
    handlers: [makeGreetHandler({ authenticate })], 
  })
  console.info('Server listening on port', port)
}

createServer().catch(console.error)

End-to-End Testing

sh
npx tsx ./src/server-first/5-authentication/server.ts

Let’s test our various use cases:

sh
$ curl 'http://localhost:6600/hello'

{
  "statusCode": 401,
  "stack": [
    "Error",
    "at createError (/Users/petite-crapouille/workspace/sacdenoeuds/extended-guide/node_modules/h3/dist/index.cjs:85:15)",
    "at Server.toNodeHandle (/Users/petite-crapouille/workspace/sacdenoeuds/extended-guide/node_modules/h3/dist/index.cjs:2279:21)",
    "at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"
  ]
}

Oopsie! Maybe our adapter needs to be updated…

Updating the H3 adapter

ts
// src/server-first/5-authentication/h3-adapter.ts

import {
  createApp,
  createRouter,
  defineEventHandler,
  getQuery,
  readBody,
  toNodeListener,
} from 'h3'
import { createServer } from 'node:http'
import { ServerAdapter } from './server-adapter'
import { Handler } from './handler'

export const createH3NodeServer: ServerAdapter = (options: {
  handlers: Handler[]
  port: number
}) => {
  const app = createApp({ debug: true })
  const router = createRouter()

  // mount the router on a base path using app.use('/base', router)
  app.use(router)

  for (const { handle, ...route } of options.handlers) {
    const routerFn = route.method === 'GET' ? router.get : router.post

    routerFn.call(
      router, // provide `router` to unbounded router method
      route.path,
      defineEventHandler(async (event) => {
        return handle({
          query: getQuery(event),
          headers: event.headers,
          body: event.headers.has('content-type')
            ? await readBody(event)
            : undefined,
          params: event.context.params ?? {},
        }).catch((error) => { 
          if (error instanceof Response) return error 
          throw error 
        })
      }),
    )
  }

  const server = createServer(toNodeListener(app))

  return new Promise<unknown>((resolve) => {
    server.listen(options.port, () => {
      resolve(server)
    })
  })
}

End-to-End Testing, Again

sh
# With no auth
$ curl 'http://localhost:6600/hello'
<div style="color: red">We don’t know you…</div>
# 🎉

# With incorrect auth
$ curl -H 'authorization: Toto' 'http://localhost:6600/hello'
<div style="color: red">We don’t know you…</div>
# ✅

# With correct auth
$ curl -H 'authorization: John' 'http://localhost:6600/hello'
<div style="color: blue">Hello, John</div>
# ✅

All good, we can move on to the next piece!