Back to guides

Own Auth with Hono


Pass Hono's Web Request directly to createOwnAuthHandler. The handler validates auth requests, sets the HttpOnly session cookie, applies CSRF checks, and returns safe errors without framework-specific auth routes.

Install Own Auth

Terminal
npm install own-auth hono @hono/node-server

Connect Postgres

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

Create the auth instance

.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 });

Protect application routes

Read the handler's session cookie and verify it with auth.getCurrentSession. Hono's context keeps the verified session available to the route, which returns only id, email, name, and imageUrl.

src/require-auth.ts
import type { CurrentSession } from "own-auth";
import { defaultSessionCookieName } from "own-auth/http";
import { getCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory";

import { auth } from "./auth";

export type AuthEnv = {
  Variables: {
    currentAuth: CurrentSession;
  };
};

export const requireAuth = createMiddleware<AuthEnv>(async (c, next) => {
  const token = getCookie(c, defaultSessionCookieName);
  const current = token ? await auth.getCurrentSession(token) : null;

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

  c.set("currentAuth", current);
  await next();
});

Mount the auth handler

src/server.ts
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { createOwnAuthHandler } from "own-auth/http";

import { auth } from "./auth";
import { type AuthEnv, requireAuth } from "./require-auth";

const app = new Hono<AuthEnv>();
const authHandler = createOwnAuthHandler(auth);

app.all("/api/auth/*", (c) => authHandler(c.req.raw));

app.get("/api/account", requireAuth, (c) => {
  const { user } = c.get("currentAuth");
  const { id, email, name, imageUrl } = user;
  return c.json({ user: { id, email, name, imageUrl } });
});

serve({ fetch: app.fetch, port: 3000 });

The mounted handler exposes the complete Own Auth HTTP API under /api/auth. The application middleware is only for routes outside that auth API.

Add the browser client

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

export const authClient = createOwnAuthClient();

For MFA, the handler keeps the challenge token in an HttpOnly cookie. The client result carries the available methods and expiry to the next screen.

src/email-password.ts
import { authClient } from "./auth-client";

export function signUp(name: string, email: string, password: string) {
  return authClient.signUpEmailPassword({ name, email, password });
}

export async function signIn(email: string, password: string) {
  const result = await authClient.signInEmailPassword({ email, password });

  if (result.status === "mfa_required") {
    return { ...result, next: "/mfa" };
  }

  const { id, email, name, imageUrl } = result.user;
  return { next: "/account", user: { id, email, name, imageUrl } };
}

export function signOut() {
  return authClient.signOut();
}

Add magic links

Email-and-password authentication does not need APP_URL. Add it with magic links because Own Auth puts the public application origin in the emailed sign-in URL. Pass it as baseUrl in the existing auth instance and configure the application's email provider before requesting a link.

.env
APP_URL=https://app.example.com
src/auth.ts
import { createOwnAuth } from "own-auth";

const tokenPepper = process.env.OWN_AUTH_TOKEN_PEPPER!;
const appUrl = process.env.APP_URL!;

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

Request and verify magic links through the same client. Complete verification sets the handler's session cookie.

src/magic-links.ts
import { authClient } from "./auth-client";

export function requestMagicLink(email: string) {
  return authClient.requestMagicLink({ email });
}

export async function verifyMagicLink(token: string) {
  const result = await authClient.verifyMagicLink({ token });

  if (result.status === "mfa_required") {
    return { ...result, next: "/mfa" };
  }

  const { id, email, name, imageUrl } = result.user;
  return { next: "/account", user: { id, email, name, imageUrl } };
}

Run on Cloudflare Workers

The Node example above uses Postgres. On Cloudflare Workers, create the Own Auth instance from the D1 binding and pass the same raw Hono request to the handler.

src/worker.ts
import { createOwnAuth } from "own-auth";
import {
  createD1Persistence,
  type D1DatabaseLike,
} from "own-auth/d1";
import { createOwnAuthHandler } from "own-auth/http";
import { Hono } from "hono";

type Bindings = {
  DB: D1DatabaseLike;
  OWN_AUTH_TOKEN_PEPPER: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.all("/api/auth/*", (c) => {
  const auth = createOwnAuth({
    ...createD1Persistence(c.env.DB),
    tokenPepper: c.env.OWN_AUTH_TOKEN_PEPPER,
  });

  return createOwnAuthHandler(auth)(c.req.raw);
});

export default app;

Generate and apply the D1 migrations with the Cloudflare D1 guide before deploying the Worker. The Hono route and browser client remain unchanged. A Worker that sends magic links also needs the APP_URL binding passed as baseUrl, as shown above.