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
npm install own-auth hono @hono/node-serverConnect Postgres
DATABASE_URL=postgres://user:password@localhost:5432/myappnpx own-auth migrateCreate the auth instance
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 });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.
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
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
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.
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.
APP_URL=https://app.example.comimport { 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.
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.
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.