Skip to content

Hono

Complete auth setup for Hono with ideal-auth.

Updated View as Markdown

This guide walks through setting up authentication in a Hono application using ideal-auth. By the end, you will have working login, registration, logout, route protection via middleware, and access to the current user in your route handlers. Hono works across runtimes, including Node.js, Bun, Cloudflare Workers, Deno, and more.

Installation

  1. Install ideal-auth

    bun add ideal-auth
  2. Set the session secret

    Add a secret to your environment. It must be at least 32 characters.

    .envbash
    IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"
    npx wrangler secret put IDEAL_AUTH_SECRET

    Or add it to your wrangler.toml for development:

    wrangler.tomltoml
    [vars]
    IDEAL_AUTH_SECRET = "at-least-32-characters-long-secret-here"

    Generate a strong secret:

    bunx ideal-auth secret

Hono provides getCookie, setCookie, and deleteCookie helpers from hono/cookie. The bridge maps these to the three functions ideal-auth expects.

src/lib/cookies.tsts
import type { Context } from 'hono';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
import type { CookieBridge } from 'ideal-auth';

export function createCookieBridge(c: Context): CookieBridge {
  return {
    get(name: string) {
      return getCookie(c, name);
    },
    set(name, value, options) {
      setCookie(c, name, value, {
        httpOnly: options.httpOnly,
        secure: options.secure,
        sameSite: options.sameSite === 'lax' ? 'Lax' : options.sameSite === 'strict' ? 'Strict' : 'None',
        path: options.path ?? '/',
        ...(options.maxAge !== undefined && { maxAge: options.maxAge }),
      });
    },
    delete(name) {
      deleteCookie(c, name, { path: '/' });
    },
  };
}

Auth setup

Create an auth() factory that accepts a Hono Context and returns an AuthInstance.

src/lib/auth.tsts
import { createAuth, createHash } from 'ideal-auth';
import { createCookieBridge } from './cookies';
import { db } from './db'; // your database client

type User = {
  id: string;
  email: string;
  name: string;
  password: string;
};

export const hash = createHash({ rounds: 12 });

export function auth(c: import('hono').Context) {
  const authFactory = createAuth<User>({
    secret: getSecret(c),
    cookie: createCookieBridge(c),
    hash,

    async resolveUser(id) {
      return db.user.findUnique({ where: { id } });
    },

    async resolveUserByCredentials(credentials) {
      return db.user.findUnique({
        where: { email: credentials.email },
      });
    },
  });

  return authFactory();
}

function getSecret(c: import('hono').Context): string {
  // Works with both env vars (Node/Bun) and Cloudflare Workers bindings
  return c.env?.IDEAL_AUTH_SECRET ?? process.env.IDEAL_AUTH_SECRET!;
}

App setup

src/index.tsts
import { Hono } from 'hono';
import { csrf } from 'hono/csrf';
import { authRoutes } from './routes/auth';
import { dashboardRoutes } from './routes/dashboard';
import { requireAuth } from './middleware/auth';

const app = new Hono();

// CSRF protection for state-changing requests
app.use(csrf());

// Public auth routes
app.route('/auth', authRoutes);

// Protected routes
app.use('/dashboard/*', requireAuth);
app.route('/dashboard', dashboardRoutes);

export default app;

Login route

src/routes/auth.tsts
import { Hono } from 'hono';
import { auth, hash } from '../lib/auth';
import { db } from '../lib/db';

export const authRoutes = new Hono();

authRoutes.post('/login', async (c) => {
  const body = await c.req.json();

  if (!body.email || !body.password) {
    return c.json({ error: 'Email and password are required.' }, 400);
  }

  const session = auth(c);
  const success = await session.attempt(
    { email: body.email, password: body.password },
    { remember: body.remember ?? false },
  );

  if (!success) {
    return c.json({ error: 'Invalid email or password.' }, 401);
  }

  return c.json({ success: true });
});

Registration route

src/routes/auth.tsts
authRoutes.post('/register', async (c) => {
  const body = await c.req.json();

  if (!body.email || !body.name || !body.password) {
    return c.json({ error: 'All fields are required.' }, 400);
  }

  if (body.password.length < 8) {
    return c.json({ error: 'Password must be at least 8 characters.' }, 400);
  }

  if (body.password !== body.passwordConfirmation) {
    return c.json({ error: 'Passwords do not match.' }, 400);
  }

  const existing = await db.user.findUnique({ where: { email: body.email } });
  if (existing) {
    return c.json({ error: 'An account with this email already exists.' }, 409);
  }

  const user = await db.user.create({
    data: {
      email: body.email,
      name: body.name,
      password: await hash.make(body.password),
    },
  });

  // Log the user in immediately after registration
  const session = auth(c);
  await session.login(user);

  return c.json({ success: true }, 201);
});

Logout route

src/routes/auth.tsts
authRoutes.post('/logout', async (c) => {
  const session = auth(c);
  await session.logout();

  return c.json({ success: true });
});

Current user route

src/routes/auth.tsts
authRoutes.get('/me', async (c) => {
  const session = auth(c);
  const user = await session.user();

  if (!user) {
    return c.json({ user: null });
  }

  return c.json({
    user: { id: user.id, email: user.email, name: user.name },
  });
});

Auth middleware

Create a middleware that protects routes from unauthenticated access and stores the user in the Hono context.

src/middleware/auth.tsts
import { createMiddleware } from 'hono/factory';
import { auth } from '../lib/auth';

type User = {
  id: string;
  email: string;
  name: string;
};

// Extend Hono's context variables
type Env = {
  Variables: {
    user: User;
  };
};

export const requireAuth = createMiddleware<Env>(async (c, next) => {
  const session = auth(c);
  const user = await session.user();

  if (!user) {
    return c.json({ error: 'Authentication required.' }, 401);
  }

  // Store non-sensitive user data in context
  c.set('user', { id: user.id, email: user.email, name: user.name });

  await next();
});

Protected route example

src/routes/dashboard.tsts
import { Hono } from 'hono';

type Env = {
  Variables: {
    user: { id: string; email: string; name: string };
  };
};

export const dashboardRoutes = new Hono<Env>();

dashboardRoutes.get('/', (c) => {
  const user = c.get('user');
  return c.json({
    message: `Welcome, ${user.name}`,
    user,
  });
});

dashboardRoutes.get('/settings', (c) => {
  const user = c.get('user');
  return c.json({
    message: 'Settings page',
    user,
  });
});

Getting the current user

In any route handler (after middleware)

app.get('/api/profile', requireAuth, (c) => {
  const user = c.get('user');
  return c.json({ user });
});

Optional auth (user may or may not be logged in)

src/middleware/optional-auth.tsts
import { createMiddleware } from 'hono/factory';
import { auth } from '../lib/auth';

type Env = {
  Variables: {
    user: { id: string; email: string; name: string } | null;
  };
};

export const optionalAuth = createMiddleware<Env>(async (c, next) => {
  const session = auth(c);
  const user = await session.user();

  c.set('user', user ? { id: user.id, email: user.email, name: user.name } : null);

  await next();
});
app.get('/', optionalAuth, (c) => {
  const user = c.get('user');
  if (user) {
    return c.json({ message: `Hello, ${user.name}` });
  }
  return c.json({ message: 'Hello, guest' });
});

CSRF protection

Hono has a built-in csrf() middleware that validates the Origin header on non-safe HTTP methods.

import { csrf } from 'hono/csrf';

// Apply to all routes
app.use(csrf());

// Or with custom options
app.use(
  csrf({
    origin: ['https://yourdomain.com', 'https://www.yourdomain.com'],
  }),
);

The csrf() middleware:

  • Allows GET, HEAD, and OPTIONS requests through
  • Validates the Origin header on POST, PUT, PATCH, and DELETE requests
  • Returns a 403 if the origin does not match

Edge runtime considerations

Hono is designed to run on edge runtimes like Cloudflare Workers. Here are the key considerations for using ideal-auth on the edge.

Cloudflare Workers

ideal-auth uses bcryptjs (pure JavaScript bcrypt) and Node.js crypto APIs for session sealing. On Cloudflare Workers:

  • bcryptjs works out of the box (pure JS, no native dependencies).
  • Node.js crypto requires the nodejs_compat compatibility flag.
wrangler.tomltoml
compatibility_flags = ["nodejs_compat"]

Environment variables

On Cloudflare Workers, access env vars via c.env:

const secret = c.env.IDEAL_AUTH_SECRET;

The getSecret helper in the auth setup above handles this automatically.

Database access

On edge runtimes, use an edge-compatible database:

  • Cloudflare D1 (SQLite at the edge)
  • Turso (distributed SQLite)
  • PlanetScale (serverless MySQL)
  • Neon (serverless Postgres)

Complete file structure

src/
  index.ts                  # Hono app entry point
  lib/
    auth.ts                 # Auth factory
    cookies.ts              # Cookie bridge
    db.ts                   # Database client
  middleware/
    auth.ts                 # requireAuth middleware
    optional-auth.ts        # Optional auth middleware
  routes/
    auth.ts                 # Login, register, logout, me
    dashboard.ts            # Protected dashboard routes

Security notes

  • Session secret: Store IDEAL_AUTH_SECRET as an environment variable. On Cloudflare Workers, use wrangler secret put. Never hard-code secrets.
  • HTTPS: On most edge platforms (Cloudflare Workers, Vercel Edge), HTTPS is enforced automatically. For Node.js/Bun deployments, set NODE_ENV=production so session cookies are marked Secure.
  • Cookie scope: The default SameSite=Lax and HttpOnly=true settings protect against CSRF and XSS cookie theft.
  • Context variables: Use Hono’s c.set() / c.get() to pass the authenticated user through the middleware chain. Use TypeScript generics (Hono<Env>) for full type safety.
  • CSRF middleware: Always enable Hono’s csrf() middleware in production. It prevents cross-origin form submissions and AJAX requests.
  • Rate limiting: Protect your login endpoint from brute-force attacks. Use ideal-auth’s createRateLimiter or Cloudflare’s built-in rate limiting for Workers.
  • bcrypt on edge: bcryptjs is CPU-intensive. On edge runtimes with strict CPU time limits (e.g., Cloudflare Workers free tier), consider reducing bcrypt rounds to 10 if you hit timeout limits, though 12 rounds is preferable for security.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close