Back to guides

Own Auth with Express


Mount createOwnAuthHandler at /api/auth before express.json(). Own Auth reads the request stream, enforces its body limit, validates the request, and writes the session cookie.

Install Own Auth

Terminal
npm install own-auth express cookie-parser
npm install --save-dev @types/express @types/cookie-parser

Set the database connection, then apply the Own Auth schema.

.env
DATABASE_URL=postgres://user:password@localhost:5432/myapp
Terminal
npx own-auth migrate

Create the auth instance

Add the token pepper used by sessions and one-time auth tokens.

.env
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secret
src/auth.ts
import { createOwnAuth } from "own-auth";

const tokenPepper = process.env.OWN_AUTH_TOKEN_PEPPER!;

export const auth = createOwnAuth({
  tokenPepper,
});

Mount the auth handler

src/server.ts
import { Readable } from "node:stream";
import express, {
  type Request as ExpressRequest,
  type Response as ExpressResponse,
} from "express";
import { createOwnAuthHandler } from "own-auth/http";

import { accountRouter } from "./account";
import { auth } from "./auth";

const app = express();
const requestContexts = new WeakMap<
  Request,
  { ipAddress?: string; userAgent?: string }
>();
const authHandler = createOwnAuthHandler(auth, {
  getRequestContext: (request) => requestContexts.get(request) ?? {},
});

app.all("/api/auth/{*path}", async (request, response) => {
  const webRequest = toWebRequest(request);
  requestContexts.set(webRequest, {
    ipAddress: request.ip,
    userAgent: request.get("user-agent"),
  });
  await sendWebResponse(response, await authHandler(webRequest));
});

app.use(express.json());
app.use(accountRouter);
app.listen(3000);

function toWebRequest(request: ExpressRequest): Request {
  if (!request.host) throw new Error("Host header is required");

  const method = request.method.toUpperCase();
  const init: RequestInit & { duplex?: "half" } = {
    method,
    headers: toWebHeaders(request),
  };

  if (method !== "GET" && method !== "HEAD") {
    init.body = Readable.toWeb(request) as ReadableStream<Uint8Array>;
    init.duplex = "half";
  }

  return new Request(
    new URL(request.originalUrl, `${request.protocol}://${request.host}`),
    init,
  );
}

function toWebHeaders(request: ExpressRequest): Headers {
  const headers = new Headers();
  for (const [name, value] of Object.entries(request.headers)) {
    if (Array.isArray(value)) {
      for (const entry of value) headers.append(name, entry);
    } else if (value !== undefined) {
      headers.set(name, value);
    }
  }
  return headers;
}

async function sendWebResponse(
  expressResponse: ExpressResponse,
  response: Response,
) {
  expressResponse.status(response.status);
  for (const [name, value] of response.headers) {
    if (name !== "set-cookie") expressResponse.setHeader(name, value);
  }

  const cookies = response.headers.getSetCookie();
  if (cookies.length) expressResponse.setHeader("set-cookie", cookies);

  expressResponse.send(Buffer.from(await response.arrayBuffer()));
}

The mounted handler exposes the complete Own Auth HTTP API under /api/auth. The context bridge keeps IP-based OAuth and One Tap limits active. Configure Express trust proxy only for proxies you operate.

Protect application routes

src/account.ts
import cookieParser from "cookie-parser";
import {
  type NextFunction,
  type Request,
  type Response,
  Router,
} from "express";
import { defaultSessionCookieName } from "own-auth/http";

import { auth } from "./auth";

export const accountRouter = Router();
accountRouter.use(cookieParser());

async function requireAuth(
  request: Request,
  response: Response,
  next: NextFunction,
) {
  const token = request.cookies[defaultSessionCookieName];
  const current = token ? await auth.getCurrentSession(token) : null;

  if (!current) {
    return response.status(401).json({ error: "Unauthorized" });
  }

  response.locals.currentAuth = current;
  next();
}

accountRouter.get("/account", requireAuth, (_request, response) => {
  const { id, email, name, imageUrl } = response.locals.currentAuth.user;
  response.json({ user: { id, email, name, imageUrl } });
});

The middleware authenticates the request once. The response exposes only id, email, name, and imageUrl; use response.locals.currentAuth.user.id for ownership and permission checks.

Add the browser client

src/auth-client.ts
import { createOwnAuthClient } from "own-auth/client";

export const authClient = createOwnAuthClient();