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:
- Quick Start Guide to create your Aura Auth instance
- TypeScript Configuration for TypeScript-specific setup
- Express Integration App for a fully working example
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.tsserver configuration - a
src/lib/handler.tsadapter that bridges Express and Aura Auth - a
src/middlewares/verify-session.tsmiddleware for protected routes - a
src/types.d.tsaugmentation forres.locals.session
Project Structure
Environment Setup
Create a .env.local file at the root of your project to store secrets securely.
# 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".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.
import { createAuth } from "@aura-stack/auth"
export const auth = createAuth({
oauth: ["github"],
basePath: "/api/auth",
})
export const { handlers, api } = authbasePath 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.
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.
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.
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:
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.
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
basePathaligned with the mounted auth route. If your auth endpoint is/api/auth/*, the auth config should usebasePath: "/api/auth". - Always convert Express headers before calling Aura Auth. The Web API session helper expects standard request headers.
- Use
session.authenticatedas 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.