← Back to blog

Use Own Auth in a NestJS application


Own Auth works in NestJS through its standard Web Request and Response handler. The NestJS application keeps its controllers and guards, while one early Express route gives Own Auth the untouched request body and returns its status, headers, cookies, and body without recreating authentication behavior in a controller.

Create one Own Auth instance

Create the instance in a server module and export it to the bootstrap route and application guards. Keep the token pepper and database connection on the server. Apply npx own-auth migrate before the new NestJS release starts.

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

export const auth = createOwnAuth({
  baseUrl: process.env.APP_URL!,
  tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER!,
  session: {
    ttlMs: 30 * 24 * 60 * 60 * 1000,
    idleTtlMs: 7 * 24 * 60 * 60 * 1000,
  },
});

Mount the handler before body parsing

Disable the automatic NestJS body parser during bootstrap. Mount the Own Auth catch all route on the underlying Express application, then add JSON parsing for normal controllers. This order lets Own Auth enforce its own content types and bounded request sizes.

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

import { AppModule } from "./app.module";
import { auth } from "./auth";

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { bodyParser: false });
  const expressApp = app.getHttpAdapter().getInstance();
  const handler = createOwnAuthHandler(auth);

  expressApp.all(
    "/api/auth/{*path}",
    async (request: ExpressRequest, response: ExpressResponse) => {
      await sendWebResponse(response, await handler(toWebRequest(request)));
    },
  );

  expressApp.use(express.json());
  await app.listen(3000);
}

function toWebRequest(request: ExpressRequest): Request {
  if (!request.host) throw new Error("Host header is required");
  const method = request.method.toUpperCase();
  const headers = new Headers();

  for (const [name, value] of Object.entries(request.headers)) {
    if (Array.isArray(value)) {
      value.forEach((entry) => headers.append(name, entry));
    } else if (value !== undefined) {
      headers.set(name, value);
    }
  }

  const init: RequestInit & { duplex?: "half" } = { method, headers };
  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,
  );
}

async function sendWebResponse(
  target: ExpressResponse,
  source: Response,
) {
  target.status(source.status);
  for (const [name, value] of source.headers) {
    if (name !== "set-cookie") target.setHeader(name, value);
  }
  const cookies = source.headers.getSetCookie();
  if (cookies.length > 0) target.setHeader("set-cookie", cookies);
  target.send(Buffer.from(await source.arrayBuffer()));
}

void bootstrap();

Do not combine repeated Set-Cookie headers into one comma separated value. OAuth, MFA, and session transitions may set or clear more than one cookie. getSetCookie() preserves each header as a separate value for Express.

Configure the proxy boundary

When NestJS runs behind a trusted proxy, configure Express trust proxy for that exact proxy. The external protocol, host, and client address must be derived from infrastructure that overwrites forwarded headers. Do not accept a client supplied forwarding chain because Own Auth uses trusted request context for security controls and audit records.

Authorize controllers from the current session

Authentication routes create the session. A NestJS guard still decides whether an application route may continue. Read the bearer token or session cookie through one trusted helper, call auth.getCurrentSession, and attach only a safe actor shape to the request. Load organisation membership and product permissions after session verification.

src/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";
import type { Request as ExpressRequest } from "express";
import { readSessionToken } from "own-auth/http";

import { auth } from "./auth";

@Injectable()
export class AuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest<ExpressRequest>();
    const headers = new Headers();
    for (const [name, value] of Object.entries(request.headers)) {
      if (Array.isArray(value)) {
        value.forEach((entry) => headers.append(name, entry));
      } else if (value !== undefined) {
        headers.set(name, value);
      }
    }
    const webRequest = new Request(
      new URL(
        request.originalUrl,
        request.protocol + "://" + request.host,
      ),
      { headers },
    );
    const { token } = readSessionToken(webRequest);
    const current = token ? await auth.getCurrentSession(token) : null;

    if (!current) throw new UnauthorizedException();
    Object.assign(request, {
      actor: { id: current.user.id, email: current.user.email },
    });
    return true;
  }
}

In a production guard, construct the Web request from the complete external URL so cookie scope and request metadata remain accurate. Do not trust a user ID or organisation ID from the request body as the actor. A valid session proves the user, while the controller or application service must still constrain every record query to a current organisation membership.

Pass trusted request context

The complete Express adapter should pass request.ip and the user agent through the handler request context. Own Auth uses trusted client context for provider limits, audit records, and session metadata. Configure trust proxy before relying on request.ip, and set it to the actual proxy count or addresses rather than enabling trust for every hop.

Keep request context outside the public body. The client does not get to declare its own IP address or user agent. When the application runs without a trusted proxy, use the socket address resolved by Express and leave unavailable metadata absent.

Use the browser client without NestJS wrappers

Frontend applications call the mounted handler with createOwnAuthClient or createOwnAuthReactClient. Do not create a second set of NestJS controllers for signup, sign in, password reset, OAuth, MFA, or passkeys. Duplicate controllers drift from the shared validation, cookie policy, error codes, and rate limits.

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

export const authClient = createOwnAuthClient({
  baseURL: "https://api.example.com/api/auth",
});

Close resources during shutdown

Register a NestJS shutdown hook that calls auth.close() after the server stops accepting new requests. This closes the package database resources cleanly during tests, rolling deployments, and local restarts. Do not close the shared instance at the end of an individual request.

Map Own Auth client errors by their stable code rather than string matching the message. Unexpected handler failures should reach the application's error reporting through the handler onError callback, with request bodies, authorization headers, cookies, OAuth parameters, and tokens removed before logging.

Verify the complete NestJS path

Run the verification with the same Express adapter and proxy settings used by the deployed server. A unit test that calls the Web handler directly cannot prove that NestJS preserved the request stream, host, protocol, origin, client address, response body, and repeated cookie headers. Include at least one browser journey through the public address and one bearer session request from a server client.

Exercise signup, sign in, MFA challenge completion, session reads, sign out, OAuth callbacks, and one guarded controller through the running NestJS server. Confirm that invalid origins fail mutation requests, multiple cookies survive the adapter, a revoked session fails on the next request, and no request body, session token, OAuth code, or provider credential appears in logs.