Aura Auth
Integrations

Express

Build your first authentication flow with Aura Auth and Express

This guide walks you through creating a complete authentication flow using Aura Auth in an Express application.

Overview

Express is built around Node.js IncomingMessage and ServerResponse streams rather than modern web-standard Request and Response objects. That means it needs a small adapter to bridge Express and Aura Auth's web-standard handlers.

Before continuing, complete the installation and initial setup:

Then use this guide to integrate Aura Auth with an Express server using best practices.


What You'll Build

You will create a small Express app with:

  • a shared src/lib/auth.ts server configuration
  • a src/lib/handler.ts adapter that bridges Express and Aura Auth
  • a src/middlewares/verify-session.ts middleware for protected routes
  • a src/types.d.ts augmentation for res.locals.session

Project Structure

auth.ts
handler.ts
verify-session.ts
types.d.ts
index.ts
tsconfig.json
package.json

Environment Setup

Create a .env.local file at the root of your project to store secrets securely.

.env.local
# 32-bytes (256-bit) secret used to sign/encrypt sessions. Use a secure random value.
AURA_AUTH_SECRET="base64-or-hex-32-bytes"
AURA_AUTH_SALT="base64-or-hex-32-bytes"
Never commit your .env.local file to version control. Use a secret manager in production.

Setup Aura Auth

Create an auth.ts file in src/lib/ to configure authentication and export the shared handlers used by the adapter and middleware.

src/lib/auth.ts
import { createAuth } from "@aura-stack/auth"

export const auth = createAuth({
  oauth: ["github"],
  basePath: "/api/auth",
})

export const { handlers, api } = auth

basePath must match the route you expose in your Express app. If the route changes, update the auth config and adapter together.

Mount HTTP Handlers

Because Express uses IncomingMessage and ServerResponse, create an adapter that converts them into the web-standard Request and Response objects Aura Auth expects.

The adapter below translates the incoming request into a Web Request, then translates the Aura Auth Response back into Express.

src/lib/handler.ts
import { handlers } from "@/lib/auth.js"
import type { Request, Response } from "express"

const splitSetCookieHeaderValue = (value: string): string[] => {
  return value
    .split(/,(?=\s*[^;,\s]+=)/g)
    .map((cookie) => cookie.trim())
    .filter(Boolean)
}

/**
 * Convert an Express request to a Web API Request so it can be handled
 * by the framework-agnostic Aura Auth handlers.
 */
export const toWebRequest = (req: Request): globalThis.Request => {
  const method = req.method ?? "GET"
  const protocol = req.protocol ?? "http"
  const host = req.get("host") ?? "localhost"

  const baseURL = `${protocol}://${host}`
  const url = new URL(req.originalUrl ?? req.url, baseURL)

  const headers = new Headers()
  for (const [key, value] of Object.entries(req.headers)) {
    if (Array.isArray(value)) {
      for (const v of value) headers.append(key, v)
    } else if (value !== undefined) {
      headers.set(key, value)
    }
  }

  const body = method !== "GET" && method !== "HEAD" && req.body !== undefined ? JSON.stringify(req.body) : undefined
  if (body !== undefined) {
    headers.set("content-type", "application/json")
  }

  return new globalThis.Request(url, {
    method,
    headers,
    body,
  })
}

/**
 * Forward the Web API Response back to the Express response.
 * Handles redirects, multiple Set-Cookie headers, and JSON/text bodies.
 */
export const toExpressResponse = async (webResponse: globalThis.Response, res: Response) => {
  for (const [key, value] of webResponse.headers.entries()) {
    if (key.toLowerCase() === "set-cookie") {
      const cookies = webResponse.headers.getSetCookie?.() ?? splitSetCookieHeaderValue(value)

      for (const cookie of cookies) {
        res.append("Set-Cookie", cookie)
      }
    } else {
      res.setHeader(key, value)
    }
  }

  res.status(webResponse.status)
  if (webResponse.status >= 300 && webResponse.status < 400) {
    const location = webResponse.headers.get("location") ?? "/"
    return res.redirect(webResponse.status, location)
  }
  const contentType = webResponse.headers.get("content-type")
  if (contentType?.includes("application/json")) {
    const data = await webResponse.json()
    return res.json(data)
  }
  const text = await webResponse.text()
  return res.send(text)
}

/**
 * Express middleware that bridges Aura Auth Web API handlers to Express.
 * Mount this on the `basePath` configured in `createAuth()` (default: `/api/auth`).
 */
export const toExpressHandler = async (req: Request, res: Response) => {
  const webResponse = await handlers.ALL(toWebRequest(req))
  return toExpressResponse(webResponse, res)
}

Mount the Aura Auth handlers inside your Express app using the Web Standard adapter.

src/index.ts
import express, { type Express } from "express"
import { toExpressHandler } from "@/lib/handler.js"

const app: Express = express()

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.all("/api/auth/*", toExpressHandler)

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000")
})

Use this route for all auth methods, including sign-in, sign-out, session lookups, and provider callbacks.

Usage

Middleware

Middlewares are a common pattern in Express apps for protecting routes. You can create a middleware that checks for an active session before allowing access to protected routes.

src/middlewares/verify-session.ts
import { auth } from "@/lib/auth.js"
import { toWebRequest } from "@/lib/handler.js"
import type { Request, Response, NextFunction } from "express"

export const verifySession = async (req: Request, res: Response, next: NextFunction) => {
  const webRequest = toWebRequest(req)
  const session = await auth.api.getSession({
    headers: webRequest.headers,
  })
  if (!session.authenticated) {
    return res.status(401).json({
      error: "Unauthorized",
      message: "You must be signed in to access this resource.",
    })
  }
  res.locals.session = session.session
  return next()
}

Express doesn't infer the Locals type automatically, so extend it to include the session:

src/types.d.ts
import type { Session } from "@aura-stack/auth"

declare global {
  namespace Express {
    interface Locals {
      session?: Session
    }
  }
}

Get Session

To access the active session in your protected Express routes, use the verifySession middleware and read res.locals.session.

src/index.ts
import express, { type Express } from "express"
import { toExpressHandler } from "@/lib/handler.js"
import { verifySession } from "@/middlewares/verify-session.js"

const app: Express = express()

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.all("/api/auth/*", toExpressHandler)

app.get("/api/protected", verifySession, (req, res) => {
  const session = res.locals.session
  return res.json({
    message: "You have access to this protected resource.",
    session,
  })
})

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000")
})

This pattern keeps the session lookup centralized and makes protected route logic easy to reuse across the app.


Common Pitfalls

  • Keep basePath aligned with the mounted auth route. If your auth endpoint is /api/auth/*, the auth config should use basePath: "/api/auth".
  • Always convert Express headers before calling Aura Auth. The Web API session helper expects standard request headers.
  • Use session.authenticated as the guard. Check that flag before exposing private data.
  • Keep the adapter and middleware separate. The adapter should only translate request and response objects, while the middleware should only enforce access.

Resources

On this page