Own Auth with Fastify
Mount createOwnAuthHandler at /api/auth. The adapter passes the raw request body, Fastify's resolved client IP, and the user agent to Own Auth for request limits, validation, CSRF checks, MFA, and session cookies.
Install Own Auth
npm install own-auth fastify @fastify/cookieSet the database connection, then apply the Own Auth schema.
DATABASE_URL=postgres://user:password@localhost:5432/myappnpx own-auth migrateCreate the auth instance
Add the token pepper used by sessions and one-time auth tokens.
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secretimport { createOwnAuth } from "own-auth";
const tokenPepper = process.env.OWN_AUTH_TOKEN_PEPPER!;
export const auth = createOwnAuth({
tokenPepper,
});Mount the auth handler
Keep the auth routes in an encapsulated plugin. Its content-type parser preserves the raw body and rejects requests over 64 KB before the handler runs.
import Fastify, { type FastifyRequest } from "fastify";
import { createOwnAuthHandler } from "own-auth/http";
import { accountRoutes } from "./account";
import { auth } from "./auth";
const maxAuthBodyBytes = 64 * 1024;
const app = Fastify({ logger: true });
const requestContexts = new WeakMap<
Request,
{ ipAddress?: string; userAgent?: string }
>();
const authHandler = createOwnAuthHandler(auth, {
maxRequestBodyBytes: maxAuthBodyBytes,
getRequestContext: (request) => requestContexts.get(request) ?? {},
});
app.register(
(routes, _options, done) => {
routes.removeAllContentTypeParsers();
routes.addContentTypeParser(
"*",
{ parseAs: "buffer", bodyLimit: maxAuthBodyBytes },
(_request, body, parseDone) => parseDone(null, body),
);
routes.setErrorHandler((error, _request, reply) => {
if (error.code === "FST_ERR_CTP_BODY_TOO_LARGE") {
return reply.code(413).send({
error: { code: "invalid_request", message: "Request body is too large" },
});
}
if (error.code === "FST_ERR_CTP_INVALID_CONTENT_LENGTH") {
return reply.code(400).send({
error: { code: "invalid_request", message: "Invalid Content-Length" },
});
}
throw error;
});
routes.all("/*", { bodyLimit: maxAuthBodyBytes }, async (request, reply) => {
const webRequest = toWebRequest(request);
requestContexts.set(webRequest, {
ipAddress: request.ip,
userAgent: request.headers["user-agent"],
});
return reply.send(await authHandler(webRequest));
});
done();
},
{ prefix: "/api/auth" },
);
await app.register(accountRoutes);
await app.listen({ host: "0.0.0.0", port: 3000 });
function toWebRequest(request: FastifyRequest): 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" && Buffer.isBuffer(request.body)) {
init.body = request.body;
init.duplex = "half";
}
return new Request(
new URL(request.raw.url ?? "/", `${request.protocol}://${request.host}`),
init,
);
}
function toWebHeaders(request: FastifyRequest): 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;
}The mounted handler exposes the complete Own Auth HTTP API under /api/auth. Configure Fastify trustProxy only for proxies you operate so request.ip, request.host, and request.protocol reflect the external request.
Protect application routes
import cookie from "@fastify/cookie";
import type { FastifyPluginAsync } from "fastify";
import type { CurrentSession } from "own-auth";
import { defaultSessionCookieName } from "own-auth/http";
import { auth } from "./auth";
declare module "fastify" {
interface FastifyRequest {
currentAuth: CurrentSession | null;
}
}
export const accountRoutes: FastifyPluginAsync = async (app) => {
await app.register(cookie);
app.decorateRequest("currentAuth", null);
app.get("/account", {
preHandler: async (request, reply) => {
const token = request.cookies[defaultSessionCookieName];
const current = token ? await auth.getCurrentSession(token) : null;
if (!current) {
return reply.code(401).send({ error: "Unauthorized" });
}
request.currentAuth = current;
},
}, async (request) => {
const { id, email, name, imageUrl } = request.currentAuth!.user;
return { user: { id, email, name, imageUrl } };
});
};The hook authenticates the request once. The response exposes only id, email, name, and imageUrl; use request.currentAuth.user.id for ownership and permission checks.
Add the browser client
import { createOwnAuthClient } from "own-auth/client";
export const authClient = createOwnAuthClient();