@chrislyons-dev/flarelette-hono
v1.1.0
Published
Type-safe JWT authentication middleware for Hono on Cloudflare Workers
Downloads
139
Maintainers
Readme
Flarelette-Hono
Framework adapter for Cloudflare Workers built on Hono + Flarelette JWT. Provides clean, declarative JWT authentication and policy enforcement for micro-APIs running on Cloudflare.
Overview
flarelette-hono is the Hono adapter for the @chrislyons-dev/flarelette-jwt toolkit.
It adds route-level middleware, context helpers, and environment injection for a fully self-contained API stack.
| Layer | Responsibility | | ---------------------------------- | ----------------------------------------------------------------- | | @chrislyons-dev/flarelette-jwt | Low-level JWT signing, verification, key handling (HS512 + EdDSA) | | flarelette-hono | Middleware, guards, request/response helpers for Hono | | Your Worker | Application logic, routes, business rules |
Design Philosophy
Flarelette-Hono is an auth middleware for Hono, not a full framework.
Unlike the Python flarelette package (which is a complete micro-framework similar to Flask), flarelette-hono is intentionally minimal:
- Python flarelette: Full framework (routing, middleware, auth, validation, logging, service factory)
- TypeScript flarelette-hono: Auth middleware for existing Hono framework (JWT auth + optional logging helper)
Why this approach?
Hono is already an excellent edge framework — we don't rewrite it. Instead, flarelette-hono adds what's missing for feature compatibility with Python flarelette:
✅ JWT authentication via authGuard() middleware
✅ Policy-based authorization via policy() builder
✅ Structured logging via optional createLogger() helper (ADR-0013 compliance)
✅ Type-safe context via HonoEnv extension
Everything else (routing, error handling, request/response, middleware chaining) is Hono's responsibility.
Features
- Framework-native: integrates seamlessly with Hono on Cloudflare Workers
- JWT middleware: declarative
authGuard(policy)for route protection - Explicit configuration:
authGuardWithConfig()for testing and multi-tenant scenarios - Role/permission policies: simple fluent builder (
policy().rolesAny().needAll()) - Env injection: automatically wires Cloudflare bindings (
env) into jwt-kit - Framework-agnostic core: built directly atop
@chrislyons-dev/flarelette-jwt - Dual configuration modes: Environment-driven or explicit config objects
- Type-safe: 100% TypeScript with strict typing — no
anytypes
Quick Start
1. Install
# Core dependencies
npm install hono @chrislyons-dev/flarelette-jwt @chrislyons-dev/flarelette-hono
# Strongly recommended: Input validation
npm install zod @hono/zod-validator
# Or with pnpm
pnpm add hono @chrislyons-dev/flarelette-jwt @chrislyons-dev/flarelette-hono
pnpm add zod @hono/zod-validator2. Configure Environment
Set required environment variables in wrangler.toml:
[vars]
JWT_ISS = "https://gateway.internal"
JWT_AUD = "your-service-name"
JWT_SECRET_NAME = "INTERNAL_JWT_SECRET"Generate and store secret:
# IMPORTANT: v1.13+ requires 64-byte minimum for HS512
npx flarelette-jwt-secret --len=64 | wrangler secret put INTERNAL_JWT_SECRET3. Minimal Example (Authentication Only)
import { Hono } from 'hono'
import { authGuard } from '@chrislyons-dev/flarelette-hono'
import type { HonoEnv } from '@chrislyons-dev/flarelette-hono'
const app = new Hono<HonoEnv>()
// Public endpoint (no auth required)
app.get('/health', (c) => c.json({ ok: true }))
// Protected endpoint (requires valid JWT)
app.get('/protected', authGuard(), async (c) => {
const auth = c.get('auth')
return c.json({ message: 'Hello', user: auth.sub })
})
export default app4. Example with Policies (Authorization)
Once you understand basic authentication, add role-based access control:
import { authGuard, policy } from '@chrislyons-dev/flarelette-hono'
// Define a policy
const analystPolicy = policy().rolesAny('analyst', 'admin').needAll('read:reports')
// Protect a route with policy
app.get('/reports', authGuard(analystPolicy), async (c) => {
const auth = c.get('auth')
return c.json({ ok: true, sub: auth.sub, roles: auth.roles })
})See API Design for complete policy builder reference.
5. Input Validation (Required for Security)
Input validation is a critical security boundary. All endpoints that accept input must validate it.
We strongly recommend using Zod for type-safe runtime validation:
npm install zod @hono/zod-validatorCombine authentication + validation:
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
// Define validation schema
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional(),
})
// Apply auth + validation middleware
app.post(
'/users',
authGuard(policy().rolesAny('admin')), // JWT auth + policy
zValidator('json', createUserSchema), // Input validation
async (c) => {
const auth = c.get('auth') // Typed JWT payload
const body = c.req.valid('json') // Typed validated input
return c.json({ ok: true })
}
)See Input Validation Guide for complete security best practices.
6. Structured Logging (Optional, Recommended for Production)
Structured logging ensures consistency across polyglot microservices (ADR-0013 compliance).
Install logging dependencies:
npm install hono-pino pinoAdd structured logging middleware:
import { createLogger } from '@chrislyons-dev/flarelette-hono'
// Add logging middleware (before auth)
app.use('*', createLogger({ service: 'bond-valuation' }))
app.use('*', authGuard())
app.post('/calculate', async (c) => {
const logger = c.get('logger')
const auth = c.get('auth')
// Structured logging with context
logger.info({ userId: auth.sub, operation: 'calculate' }, 'Processing calculation')
return c.json({ result: 42 })
})Output (JSON):
{
"timestamp": "2025-11-02T12:34:56.789Z",
"level": "info",
"service": "bond-valuation",
"requestId": "a3f2c1b0-1234-5678-9abc",
"userId": "auth0|123",
"operation": "calculate",
"msg": "Processing calculation"
}See Structured Logging Guide for ADR-0013 standards and best practices.
7. Test Your Setup
Start development server:
wrangler devTest protected endpoint:
# This will return 401 (expected - no JWT)
curl http://localhost:8787/protected
# Generate test JWT and make authenticated request
# (See examples/ directory for token generation utilities)8. Wrangler Config Example
name = "bond-consumer"
main = "src/app.ts"
compatibility_date = "2025-10-01"
[[services]]
binding = "GATEWAY"
service = "bond-gateway"
[vars]
JWT_ISS = "https://gateway.internal"
JWT_AUD = "bond-math.api"Explicit Configuration (New!)
The traditional authGuard() middleware reads JWT configuration from environment variables (JWT_ISS, JWT_AUD, JWT_SECRET_NAME, etc.). This works well for most cases but can be challenging for:
- Testing: Setting up environment variables for unit tests
- Multi-tenant apps: Different JWT configs for different routes
- Development: Avoiding global state mutation
Solution: Use authGuardWithConfig() with explicit configuration objects:
import { authGuardWithConfig, createHS512Config } from '@chrislyons-dev/flarelette-hono'
// Create explicit JWT configuration
const config = createHS512Config(
'your-base64url-encoded-secret', // Base64url-encoded secret (32+ bytes)
{
iss: 'https://auth.example.com',
aud: 'api.example.com',
}
)
// Use in middleware (no environment variables needed!)
app.get('/protected', authGuardWithConfig(config), async (c) => {
const auth = c.get('auth')
return c.json({ user: auth.sub })
})
// Works with policies too
const adminPolicy = policy().rolesAny('admin').build()
app.get('/admin', authGuardWithConfig(config, adminPolicy), async (c) => {
return c.json({ admin: true })
})Multi-Tenant Example
Different routes can use different JWT configurations in the same process:
const tenantAConfig = createHS512Config(secretA, {
iss: 'https://auth-tenant-a.example.com',
aud: 'tenant-a',
})
const tenantBConfig = createHS512Config(secretB, {
iss: 'https://auth-tenant-b.example.com',
aud: 'tenant-b',
})
app.get('/tenant-a/*', authGuardWithConfig(tenantAConfig), tenantAHandlers)
app.get('/tenant-b/*', authGuardWithConfig(tenantBConfig), tenantBHandlers)Testing Benefits
Explicit configuration makes testing much easier:
import { createHS512Config, authGuardWithConfig } from '@chrislyons-dev/flarelette-hono'
import { SignJWT } from 'jose'
// Test setup (no environment pollution!)
const testSecret = Buffer.alloc(32, 42).toString('base64url')
const testConfig = createHS512Config(testSecret, {
iss: 'test-issuer',
aud: 'test-audience',
})
// Create test token
const token = await new SignJWT({ sub: 'test-user' })
.setProtectedHeader({ alg: 'HS512' })
.setIssuer('test-issuer')
.setAudience('test-audience')
.setIssuedAt()
.setExpirationTime('1h')
.sign(Buffer.from(testSecret, 'base64url'))
// Test authenticated request
const res = await app.request('/protected', {
headers: { Authorization: `Bearer ${token}` },
})When to use each approach:
- Environment-based (
authGuard): Production deployments, standard Workers with env vars - Explicit config (
authGuardWithConfig): Testing, multi-tenant apps, development, avoiding global state
Both approaches are fully supported and can be mixed in the same application!
Gateway and Service Mesh Architecture
Typical flarelette deployments use a gateway pattern where external OIDC tokens are verified at the gateway, which then mints internal tokens for the service mesh.
Both gateway and internal services use flarelette-hono — the only difference is the JWKS resolution strategy:
| Component | JWKS Source | Purpose |
|-----------|-------------|---------|
| Gateway | JWT_JWKS_URL (HTTP) | Verify external OIDC tokens (Auth0, Okta, CF Access) |
| Internal Services | JWT_JWKS_SERVICE_NAME (binding) | Verify internal tokens from gateway (fast RPC) |
Gateway Example
import { Hono } from 'hono'
import { authGuard } from '@chrislyons-dev/flarelette-hono'
import { sign } from '@chrislyons-dev/flarelette-jwt'
import type { HonoEnv } from '@chrislyons-dev/flarelette-hono'
const app = new Hono<HonoEnv>()
// Verify external OIDC tokens via JWT_JWKS_URL
app.post('/token-exchange', authGuard(), async (c) => {
const externalAuth = c.get('auth')
// Mint internal token for service mesh
const internalToken = await sign({
sub: externalAuth.sub,
email: externalAuth.email,
roles: deriveRoles(externalAuth),
permissions: derivePermissions(externalAuth)
})
return c.json({ token: internalToken })
})# Gateway wrangler.toml
[vars]
JWT_ISS = "https://auth0.example.com/"
JWT_AUD = "my-app-client-id"
JWT_JWKS_URL = "https://auth0.example.com/.well-known/jwks.json"
# For minting internal tokens
JWT_PRIVATE_JWK_NAME = "GATEWAY_PRIVATE_KEY"Internal Service Example
import { Hono } from 'hono'
import { authGuard } from '@chrislyons-dev/flarelette-hono'
import type { HonoEnv } from '@chrislyons-dev/flarelette-hono'
const app = new Hono<HonoEnv>()
// Verify internal tokens via service binding
app.get('/data', authGuard(), async (c) => {
const auth = c.get('auth')
return c.json({ data: [], user: auth.sub })
})# Service wrangler.toml
[[services]]
binding = "GATEWAY"
service = "jwt-gateway"
[vars]
JWT_ISS = "https://gateway.internal"
JWT_AUD = "bond-math.api"
JWT_JWKS_SERVICE_NAME = "GATEWAY"See Configuration Guide for complete examples.
Cloudflare Access Integration
Workers behind Cloudflare Access can authenticate users via the CF-Access-Jwt-Assertion header. This header contains a JWT issued by Cloudflare after successful authentication.
Note: Cloudflare Access uses standard JWKS format (RFC 7517) but with a non-standard path (/cdn-cgi/access/certs). It does not provide an OIDC discovery endpoint. This is fine — authGuard() supports direct JWKS URLs.
How It Works
Cloudflare Access acts as an authentication layer before requests reach your Worker:
- User authenticates via Access (SSO, OIDC, etc.)
- Cloudflare injects
CF-Access-Jwt-Assertionheader with signed JWT - Your Worker validates the JWT using
authGuard()
No code changes required — authGuard() automatically checks both Authorization and CF-Access-Jwt-Assertion headers.
Header Precedence
When both headers are present, Authorization takes priority:
// Authorization header is checked first
// CF-Access-Jwt-Assertion is used as fallback if Authorization is missing or invalidThis allows you to:
- Use Cloudflare Access for browser-based users (
CF-Access-Jwt-Assertion) - Use API tokens for service-to-service calls (
Authorization: Bearer)
Configuration
Configure authGuard() to verify Cloudflare Access JWTs:
import { authGuard } from '@chrislyons-dev/flarelette-hono'
import type { HonoEnv } from '@chrislyons-dev/flarelette-hono'
const app = new Hono<HonoEnv>()
// Works with both Authorization and CF-Access-Jwt-Assertion headers
app.get('/protected', authGuard(), async (c) => {
const auth = c.get('auth')
return c.json({ user: auth.sub, email: auth.email })
})Environment variables:
# wrangler.toml
[vars]
JWT_ISS = "https://your-team.cloudflareaccess.com" # Cloudflare Access issuer
JWT_AUD = "your-application-aud-tag" # Application audience tagGet your team domain and audience tag from the Cloudflare Zero Trust dashboard.
Verify Cloudflare Access Tokens
Set up JWKS verification for Cloudflare Access public keys:
# wrangler.toml
[vars]
JWT_ISS = "https://your-team.cloudflareaccess.com"
JWT_AUD = "your-application-aud-tag"
# JWKS URL is a public endpoint - no need for secret
JWT_JWKS_URL = "https://your-team.cloudflareaccess.com/cdn-cgi/access/certs"Security note: Cloudflare Access uses short-lived JWTs (typically 1 hour). Set appropriate JWT_LEEWAY to handle clock skew.
Mixed Authentication Example
Support both Cloudflare Access (browser) and API tokens (service-to-service):
// Browser users → Cloudflare Access → CF-Access-Jwt-Assertion header
// API clients → Direct token → Authorization: Bearer header
app.get('/data', authGuard(), async (c) => {
const auth = c.get('auth')
// Both authentication methods provide the same JwtPayload structure
return c.json({
user: auth.sub,
email: auth.email,
source: c.req.header('CF-Access-Jwt-Assertion') ? 'access' : 'api'
})
})Limitations
- Same JWT structure required: Both authentication methods must use compatible JWT payloads
- Single issuer:
JWT_ISSapplies to both Authorization and CF-Access tokens - No automatic header selection: Use header precedence (Authorization first) rather than route-based selection
For separate issuers or different JWT structures, use authGuardWithConfig() on different route groups.
Next Steps
- Basic usage: See examples above for authentication and policies
- Advanced authorization: Read API Design for complex policies
- Production deployment: Review JWT Integration for EdDSA setup
- Testing: See CONTRIBUTING.md for test patterns
API Summary
authGuard(policy?)
Hono middleware that:
- extracts JWT from
Authorization: Bearer <jwt>orCF-Access-Jwt-Assertionheader - validates via jwt-kit
- enforces the given policy (if provided)
- injects the verified claims into
c.set('auth', payload)
Header Precedence: Authorization is checked first; CF-Access-Jwt-Assertion is used as fallback.
If validation fails → returns 401 Unauthorized or 403 Forbidden.
import { authGuard, policy } from '@chrislyons-dev/flarelette-hono'
// Require authentication only
app.get('/protected', authGuard(), async (c) => {
const auth = c.get('auth')
return c.json({ user: auth.sub })
})
// Require authentication + policy
app.get('/admin', authGuard(policy().rolesAny('admin')), async (c) => {
return c.json({ message: 'Admin only' })
})policy()
Fluent builder for permission rules:
policy()
.rolesAny('admin', 'analyst') // At least one role required
.rolesAll('verified', 'approved') // All roles required
.needAny('read:data', 'read:reports') // At least one permission required
.needAll('write:reports', 'audit:log') // All permissions requiredContext Helpers
import type { AuthContext } from '@chrislyons-dev/flarelette-hono'
app.get('/data', authGuard(), async (c) => {
// Access verified JWT payload
const auth: AuthContext = c.get('auth')
// auth.sub: string
// auth.iss: string
// auth.aud: string
// auth.roles?: string[]
// auth.permissions?: string[]
// auth.actor?: ActorClaim
})Configuration (shared with jwt-kit)
| Variable | Description |
| ------------------------------------------ | ------------------------------------------------------- |
| JWT_ISS, JWT_AUD | Expected issuer & audience |
| JWT_TTL_SECONDS, JWT_LEEWAY | Defaults: 900 / 90 |
| JWT_SECRET_NAME / JWT_SECRET | HS512 secret |
| JWT_PRIVATE_JWK_NAME / JWT_PRIVATE_JWK | EdDSA signing key (gateway) |
| JWT_JWKS_URL | JWKS endpoint (public URL, use for OIDC/Access) |
| JWT_JWKS_URL_NAME | Secret name containing JWKS URL (rarely needed) |
| setJwksResolver() | For internal Service Bindings (no public JWKS endpoint) |
Documentation
- Architecture - System design and component overview
- API Design - Complete API reference and examples
- JWT Integration - Token structure, configuration strategies, and patterns
- Input Validation - Security best practices for validating all input with Zod
- Structured Logging - ADR-0013 compliant logging for polyglot microservices
- Contributing - Development setup and guidelines
Testing Tips
Run integration tests in Miniflare with
JWT_SECRETor a stubbed resolverUse
@chrislyons-dev/flarelette-jwtCLI to generate 64-byte secrets for HS512Mock the JWKS resolver in local tests:
import { setJwksResolver } from '@chrislyons-dev/flarelette-jwt' setJwksResolver(async () => ({ keys: [mockPublicJwk] }))
Roadmap
- [ ] Optional mTLS / Access integration for external JWKS
- [ ] KV-backed replay store (
jti) - [ ] Rich error handling hooks (
onUnauthorized,onForbidden) - [ ] OpenAPI/Swagger integration
Security First
JWT authentication and input validation are critical security boundaries. This library prioritizes security over convenience:
Security Requirements (v1.13+)
HS512 Secret Minimum Length ⚠️
CRITICAL: As of flarelette-jwt v1.13, HS512 secrets must be at minimum 64 bytes (512 bits). Shorter secrets will cause configuration errors at startup.
Generate secure secrets:
# Required: 64-byte minimum for HS512
npx flarelette-jwt-secret --len=64Why 64 bytes?
- Matches SHA-512 digest size (512 bits)
- Prevents brute-force attacks on weak secrets
- Industry best practice for HMAC-SHA-512
- Breaking change from v1.12 and earlier
Error if secret too short:
Error: JWT secret too short: 32 bytes, need >= 64 for HS512 (use 'npx flarelette-jwt-secret --len=64')Mode Exclusivity (Algorithm Confusion Prevention)
CRITICAL: You cannot configure both HS512 and asymmetric (EdDSA/RSA) modes simultaneously. This prevents algorithm confusion attacks (CVE-2015-9235).
Choose ONE mode:
- ✅ HS512:
JWT_SECRET_NAMEorJWT_SECRET - ✅ EdDSA:
JWT_PRIVATE_JWK_NAME+JWT_PUBLIC_JWK_NAME - ✅ RSA (external OIDC):
JWT_JWKS_URL - ❌ HS512 + EdDSA: Configuration error
Error if both configured:
Configuration error: Both HS512 (JWT_SECRET) and asymmetric (JWT_PUBLIC_JWK/JWT_JWKS_*) secrets configured. Choose one to prevent algorithm confusion attacks.Authentication Security
- Fail securely: Invalid tokens return
401, insufficient permissions return403 - No detail leakage: Error messages never expose token structure or validation details
- Short-lived tokens: 5-15 minute TTL recommended
- Audience validation: Prevents token reuse across services
- Algorithm whitelisting: HS512 mode only allows
['HS512'], EdDSA/RSA mode only allows['EdDSA', 'RS256', 'RS384', 'RS512'] - No
alg: none: Thenonealgorithm is never supported (CVE-2015-2951) - JWKS injection prevention: JWKS URLs pinned in config, never read from token headers
Input Validation Security
- Validate all input: Every endpoint that accepts data must validate it
- Use Zod: Type-safe runtime validation prevents injection attacks and type confusion
- Constrain everything: String lengths, array sizes, numeric ranges, formats
- Never trust input: Even from authenticated users
Type Safety
- 100% strict TypeScript: No
anytypes throughout the codebase - Runtime + compile-time validation: Zod provides both type inference and runtime checks
- Strong typing prevents vulnerabilities: Type confusion and injection attacks are mitigated
See JWT Integration Guide for detailed security considerations and the flarelette-jwt Security Guide for complete cryptographic details.
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for:
- Development setup
- Code style guidelines (strict TypeScript, no
any) - Testing requirements
- Pull request process
License
MIT © Chris Lyons
Part of the Flarelette micro-API toolkit for Cloudflare Workers.
Related Projects
- @chrislyons-dev/flarelette-jwt - JWT toolkit for Cloudflare Workers
- Hono - Ultrafast web framework for the edge
- Cloudflare Workers - Serverless platform
