Hono
Hono is the primary supported framework. Workers Auth returns Hono apps and middleware directly — no adapters, no wrappers. You mount the handler, apply middleware, and you’re done.
import { WorkersAuth } from 'workers-auth';
const auth = WorkersAuth({ /* config */ });// auth.handler — Hono app with login/logout/session routes// auth.admin — Hono app with user management endpoints// auth.authenticate — middleware that validates sessions// auth.authorize — middleware factory for role/permission checksMounting the handler
Section titled “Mounting the handler”auth.handler is a full Hono app. Mount it at /auth to register all authentication routes:
app.route('/auth', auth.handler);This single line gives you every route your frontend needs:
| Route | Method | Description |
|---|---|---|
/auth/magic-link/send | POST | Send a magic link email |
/auth/magic-link/verify | GET | Verify token and create session |
/auth/github | GET | Start GitHub OAuth flow |
/auth/github/callback | GET | Handle GitHub OAuth callback |
/auth/session | GET | Get current session/user |
/auth/logout | POST | Destroy session |
Which routes appear depends on which strategies you configure. The session and logout routes are always present.
Authentication middleware
Section titled “Authentication middleware”auth.authenticate is Hono middleware that reads the session cookie, validates it, and makes the user available to downstream handlers. Apply it to any route group that requires a logged-in user:
app.use('/api/*', auth.authenticate);If the session is missing or invalid, it returns 401 Unauthorized and stops the request. If the user’s account has been banned, it revokes the session and returns 401 as well.
On success, two values are set on the Hono context:
| Key | Type | Description |
|---|---|---|
c.get('user') | User | The authenticated user object |
c.get('sessionId') | string | The current session ID |
Authorization middleware
Section titled “Authorization middleware”auth.authorize(type, value) returns Hono middleware that checks whether the authenticated user meets a specific role or permission requirement.
// Require the "admin" roleapp.use('/admin/*', auth.authorize('role', 'admin'));
// Require the "write" permissionapp.post('/api/posts', auth.authorize('permission', 'write'), createPost);The authenticate middleware must run first — authorize reads the user from context. If the check fails, it returns 403 Forbidden.
// Correct ordering: authenticate before authorizeapp.use('/api/*', auth.authenticate);app.use('/api/admin/*', auth.authorize('role', 'admin'));Accessing the user
Section titled “Accessing the user”After auth.authenticate runs, the User object is available via c.get('user'):
app.get('/api/me', (c) => { const user = c.get('user'); return c.json(user);});The User object:
interface User { id: string; email: string; status: 'active' | 'banned'; createdAt: number; updatedAt: number;}Admin handler
Section titled “Admin handler”auth.admin is a separate Hono app that exposes user management endpoints — listing users, banning/unbanning, and managing role assignments. Mount it wherever you want, and protect it with your own middleware:
app.use('/admin/*', auth.authenticate);app.use('/admin/*', auth.authorize('role', 'admin'));app.route('/admin', auth.admin);This registers the following routes:
| Route | Method | Description |
|---|---|---|
/admin/users | GET | List users (supports ?limit= and ?offset=) |
/admin/users/:id | GET | Get a single user with their roles |
/admin/users/:id/ban | POST | Ban user and revoke all sessions |
/admin/users/:id/unban | POST | Unban user |
/admin/users/:id/roles | POST | Assign a role (body: { "role": "editor" }) |
/admin/users/:id/roles/:role | DELETE | Remove a role |
The admin handler has no built-in access control. You decide who can reach it by applying authenticate and authorize before mounting.
Type-safe bindings
Section titled “Type-safe bindings”Hono supports generic type parameters for your Worker’s environment bindings. Define your Env type once and pass it to new Hono() for full type safety across handlers:
type Env = { Bindings: { DB: D1Database; SESSIONS: KVNamespace; RESEND_API_KEY: string; GITHUB_CLIENT_ID: string; GITHUB_CLIENT_SECRET: string; };};
const app = new Hono<Env>();This gives you autocomplete and type checking when accessing c.env.DB, c.env.SESSIONS, and your secrets.
Full example
Section titled “Full example”A complete Worker using magic links, GitHub OAuth, RBAC, and the admin API:
import { Hono } from 'hono';import { WorkersAuth } from 'workers-auth';import { MagicLinkStrategy } from 'workers-auth/authn/magic-link';import { GitHubStrategy } from 'workers-auth/authn/github';import { RBACPolicy } from 'workers-auth/authz/rbac';import { ResendProvider } from 'workers-auth/providers/resend';import { D1Adapter } from 'workers-auth/adapters/d1';import { KVAdapter } from 'workers-auth/adapters/kv';import { defaultTemplate } from 'workers-auth/templates/default';
type Env = { Bindings: { DB: D1Database; SESSIONS: KVNamespace; RESEND_API_KEY: string; GITHUB_CLIENT_ID: string; GITHUB_CLIENT_SECRET: string; };};
const app = new Hono<Env>();
const auth = WorkersAuth({ database: (binding) => D1Adapter(binding), cache: (binding) => KVAdapter(binding), authn: { strategies: [ MagicLinkStrategy({ provider: ResendProvider({ apiKey: process.env.RESEND_API_KEY!, from: 'auth@yourdomain.com', }), template: defaultTemplate, }), GitHubStrategy({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), ], session: { secret: 'your-secret-key-here' }, }, authz: RBACPolicy({ roles: { admin: ['*'], editor: ['read', 'write'], user: ['read'], }, defaultRole: 'user', }), redirectUrl: '/dashboard',});
// Auth routes — login, logout, sessionapp.route('/auth', auth.handler);
// All API routes require a valid sessionapp.use('/api/*', auth.authenticate);
// Admin routes require the admin roleapp.use('/admin/*', auth.authenticate);app.use('/admin/*', auth.authorize('role', 'admin'));app.route('/admin', auth.admin);
// Route handlersapp.get('/api/me', (c) => c.json(c.get('user')));
app.get('/api/posts', auth.authorize('permission', 'read'), (c) => { return c.json({ posts: [] });});
app.post('/api/posts', auth.authorize('permission', 'write'), async (c) => { const body = await c.req.json(); return c.json({ created: true });});
export default app;Deploy with wrangler deploy and the D1 tables are created automatically on the first request.