The official Auth0 SDK for the Hono web framework — login, logout, session management, token access, and route protection as native Hono middleware. Works across Node.js, Cloudflare Workers, Bun, Deno, and Vercel Edge.
Hono is one of the fastest-growing web frameworks in the JS ecosystem, running everywhere — edge, serverless, traditional servers — with a unified API. This SDK brings Auth0 authentication to Hono with zero setup: one auth0() middleware call and auth just works.
Built on the foundation of @auth0/auth0-server-js, this SDK provides Hono-idiomatic middleware for authentication, authorization, session management, and token handling — without rewriting OIDC code.
npm install @auth0/auth0-honoimport { Hono } from 'hono'
import { auth0, requiresAuth } from '@auth0/auth0-hono'
const app = new Hono()
// Add auth to every route
app.use('*', auth0())
// Public route
app.get('/', (c) => c.text('Home'))
// Protected route
app.get('/profile', requiresAuth(), (c) => {
const user = c.var.auth0.user
return c.json({ name: user?.name, sub: user?.sub })
})
export default appThe SDK reads configuration from Hono's environment (works across all runtimes — Node.js, CF Workers, Bun, Deno):
| Variable | Required | Description |
|---|---|---|
AUTH0_DOMAIN |
Yes | Auth0 domain (e.g., tenant.auth0.com) |
AUTH0_CLIENT_ID |
Yes | Auth0 application client ID |
AUTH0_CLIENT_SECRET |
No | Client secret (required for refresh token flow) |
AUTH0_SESSION_ENCRYPTION_KEY |
Yes | 32+ character encryption key for session cookies |
APP_BASE_URL |
Yes | Base URL of your application (e.g., https://myapp.com) |
.env example:
AUTH0_DOMAIN=tenant.auth0.com
AUTH0_CLIENT_ID=abc123
AUTH0_CLIENT_SECRET=secret123
AUTH0_SESSION_ENCRYPTION_KEY=very_long_string_with_at_least_32_characters
APP_BASE_URL=https://myapp.com
Override or augment environment variables with explicit config:
app.use(
'*',
auth0({
domain: 'tenant.auth0.com',
clientID: 'abc123',
clientSecret: 'secret123',
baseURL: 'https://myapp.com',
session: {
secret: 'your_32_char_secret_key_here',
cookie: {
name: 'auth_session',
sameSite: 'lax',
secure: true,
},
},
authorizationParams: {
scope: 'openid profile email',
audience: 'https://api.myapp.com',
},
})
)Config precedence: explicit config > environment variables > schema defaults
Known limitation: Configuration is captured from the first request (singleton pattern). Multi-tenant deployments where different requests have different environment bindings (e.g., Cloudflare Workers with per-route env) should use separate
auth0()middleware instances with explicit config objects rather than relying on environment variables.
Main middleware — sets up routes, session management, and context population.
app.use('*', auth0())What it handles automatically:
- Login/callback/logout routes (
/auth/login,/auth/callback,/auth/logout) - Backchannel logout
- Session encryption and cookie management
- User data available on every request via
c.var.auth0.user - Token refresh (transparent, deduplicated)
Options:
{
domain?: string // Auth0 domain
clientID?: string // Client ID
clientSecret?: string // Client secret
baseURL?: string // App base URL
session?: {
secret: string | string[] // Encryption key(s) — supports rotation
cookie?: {
name?: string // Default: 'appSession'
domain?: string
sameSite?: 'lax' | 'strict' | 'none'
secure?: boolean
}
store?: SessionStore // Custom session store (optional)
}
authorizationParams?: Record<string, any> // Scope, audience, etc.
routes?: {
login?: string // Default: '/auth/login'
callback?: string // Default: '/auth/callback'
logout?: string // Default: '/auth/logout'
backchannelLogout?: string // Default: '/auth/backchannel-logout'
}
onCallback?: (c, error, session) => ... // Post-login hook (see Hooks below)
attemptSilentLogin?: boolean // Default: false
fetch?: typeof global.fetch // Custom fetch (optional)
}Enforce authentication on protected routes. Returns 401 on unauthenticated requests.
app.get('/dashboard', requiresAuth(), (c) => {
// c.var.auth0.user is guaranteed to exist here
return c.json(c.var.auth0.user)
})Enforce organization membership. Throws AccessDeniedError if user is not in the specified organization.
// Any organization
app.get('/admin', requiresAuth(), requiresOrg(), handler)
// Specific organization
app.get('/admin', requiresAuth(), requiresOrg({ orgId: 'org_123' }), handler)
// Custom check
app.get('/admin', requiresAuth(), requiresOrg((c) => {
return c.var.auth0.user?.org_id === 'org_123'
}), handler)Check if a claim equals an expected value.
app.get('/admin',
requiresAuth(),
claimEquals('role', 'admin'),
handler
)Check if a claim array includes any of the provided values.
app.get('/reports',
requiresAuth(),
claimIncludes('permissions', 'read:reports', 'admin:reports'),
handler
)Custom claim validation function.
app.get('/restricted',
requiresAuth(),
claimCheck((user) => user.email_verified === true),
handler
)Retrieve the full session object. Returns null if unauthenticated.
const session = await getSession(c)
if (session) {
console.log(session.user.email)
}Get the authenticated user. Throws MissingSessionError if not authenticated.
const user = getUser(c)
console.log(user.name)Get an access token. Automatically refreshes if expired. The token audience is determined by authorizationParams.audience in the auth0() middleware config.
const { accessToken } = await getAccessToken(c)
// Use in API call
const res = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${accessToken}` }
})Token deduplication: If 5 parallel requests call getAccessToken() and a refresh is needed, only 1 refresh request is made. Others await the same promise.
Note: For connection-scoped tokens (e.g., Google OAuth2), use
getAccessTokenForConnection()instead.
Get a token for a specific connection (for service-to-service communication).
const token = await getAccessTokenForConnection(c, {
connection: 'google-oauth2',
loginHint: 'user@example.com'
})Merge custom data into the session. Reserved fields (user, idToken, refreshToken, internal) are protected.
await updateSession(c, {
permissions: ['read:data', 'write:data'],
customField: 'custom value'
})
// Now available on all subsequent requests
const perms = c.var.auth0.session?.permissionsUse authentication handlers without the auth0() middleware:
import {
handleLogin,
handleLogout,
handleCallback,
handleBackchannelLogout
} from '@auth0/auth0-hono'
// Mount handlers on custom routes
app.get('/login', handleLogin())
app.get('/logout', handleLogout())
app.get('/callback', handleCallback())
app.post('/logout-notify', handleBackchannelLogout())These resolve configuration from environment variables automatically.
Run custom logic after a successful login or on login error. Use for session enrichment, error customization, or logging.
app.use('*', auth0({
async onCallback(c, error, session) {
if (error) {
// Error path: return custom error page or response
return c.redirect('/login?error=true')
}
// Success path: enrich session with custom data
const permissions = await fetchUserPermissions(session.user.sub)
return {
...session,
permissions
}
}
}))Contract:
- Success:
errorisnull,sessionis populated. Return enrichedSessionDataorResponse. - Error:
errorisAuth0Error,sessionisnull. ReturnResponseto override error page. Return value ignored otherwise. - Promise rejection in hook: Original error always propagates.
The SDK throws typed errors that extend Hono's HTTPException. Catch and handle them in app.onError:
import {
Auth0Error,
AccessDeniedError,
LoginRequiredError,
InvalidGrantError
} from '@auth0/auth0-hono'
app.onError((err, c) => {
if (err instanceof AccessDeniedError) {
return c.json({ error: 'Access denied' }, 403)
}
if (err instanceof LoginRequiredError) {
return c.redirect('/auth/login')
}
if (err instanceof InvalidGrantError) {
return c.json({ error: 'Token expired, please log in again' }, 401)
}
if (err instanceof Auth0Error) {
return c.json(
{ error: err.code, error_description: err.description },
err.status
)
}
// Other errors
return c.json({ error: 'Internal server error' }, 500)
})| Class | HTTP Status | Code | When Thrown |
|---|---|---|---|
Auth0Error |
500 | unknown_error |
Base class — catch-all |
LoginRequiredError |
401 | login_required |
requiresAuth() on unauthenticated request |
AccessDeniedError |
403 | access_denied |
Authorization check failed (claims, organization) |
InvalidGrantError |
401 | invalid_grant |
Refresh token expired or invalid |
MissingSessionError |
401 | missing_session |
getUser() called without session |
MissingTransactionError |
400 | missing_transaction |
Callback without login transaction |
TokenRefreshError |
401 | token_refresh_error |
Token refresh failed |
ConnectionTokenError |
401 | connection_token_error |
Connection token request failed |
All errors respond with OAuth2-compliant JSON:
{
"error": "access_denied",
"error_description": "User does not belong to the required organization"
}This SDK works across multiple JavaScript runtimes:
| Runtime | Level | Status |
|---|---|---|
| Node.js 18+ | Primary | Full support |
| Cloudflare Workers | Primary | Full support |
| Bun 1.x+ | Secondary | Works, best-effort testing |
| Deno 1.x/2.x | Secondary | Works, best-effort testing |
| Vercel Edge | Secondary | Works, best-effort testing |
Key: The SDK uses Hono's env(c) adapter for all environment variable access, making it runtime-agnostic. No process.env anywhere on the critical path.
Full TypeScript support with types for context, session, user, and tokens.
Use OIDCEnv for strict typing of middleware handlers:
import { OIDCEnv, requiresAuth } from '@auth0/auth0-hono'
app.get('/protected', requiresAuth(), (c: Context<OIDCEnv>) => {
// c.var.auth0 is fully typed and non-null here
const user = c.var.auth0.user
const session = c.var.auth0.session
const org = c.var.auth0.org
return c.json({ user, session, org })
})To get autocomplete on c.var.auth0 globally, import the type augmentation:
import '@auth0/auth0-hono/lib/honoEnv'
app.get('/', (c) => {
// c.var.auth0 now has autocomplete (but still optional — null check required)
if (c.var.auth0?.user) {
return c.json(c.var.auth0.user)
}
})// User claims
export interface Auth0User extends UserClaims {
sub: string // Subject (user ID)
name?: string
email?: string
email_verified?: boolean
org_id?: string // Organization ID (if in org)
org_name?: string // Organization name (if in org)
[key: string]: any // Custom claims
}
// Organization context
export interface Auth0Organization {
id: string
name?: string
}
// Full session (all tokens, user, custom fields)
export interface Auth0Session {
user: Auth0User
idToken: string
refreshToken?: string
tokenSets: TokenSet[]
// + custom fields from updateSession()
[key: string]: unknown
}
// Main context variable
export interface Auth0Context {
user: Auth0User | null
session: Auth0Session | null
org: Auth0Organization | null
}
// Token set
export type Auth0TokenSet = {
accessToken: string
audience: string
scope?: string
expiresAt: number
}For detailed API documentation, see DESIGN.md (technical spec) and BETA-OVERVIEW.md (feature overview).
Ensure you're using environment variables correctly in your wrangler.toml:
[env.production]
vars = { AUTH0_DOMAIN = "tenant.auth0.com", AUTH0_CLIENT_ID = "abc123" }The SDK uses Hono's env(c) adapter, which correctly reads CF Workers bindings.
If you're enriching sessions with large data via updateSession(), consider using a custom stateful session store:
import { SessionStore } from '@auth0/auth0-hono'
const customStore: SessionStore = {
async set(name, data, isTransaction, ctx) {
// Store session data in your database
await db.sessions.set(data.internal.sid, data)
},
async get(name, ctx) {
// Retrieve from database
return await db.sessions.get(sessionId)
},
// ... delete, clear
}
app.use('*', auth0({
session: { secret: '...', store: customStore }
}))Ensure AUTH0_CLIENT_SECRET is set and that the client has offline access enabled in your Auth0 dashboard.
We appreciate feedback and contributions! Please see CONTRIBUTING.md and CODE_OF_CONDUCT.md.
Please do not report security vulnerabilities on GitHub. Use Auth0's Responsible Disclosure Program.
This project is licensed under the MIT License. See LICENSE file for details.

Auth0 is an easy to implement, adaptable authentication and authorization platform. Learn more at Why Auth0?