Package Exports
- bezzie
- bezzie/cloudflare
- bezzie/memory
- bezzie/redis
Readme
Bezzie
Bezzie is a BFF (Backend for Frontend) OAuth 2.0 library for Cloudflare Workers. It implements BCP212 — your frontend never sees a JWT.
Bezzie — your BFF's BFF. Handles the Backend for Frontend OAuth pattern so you don't have to.
The BFF owns the OAuth flow and issues a session cookie to the frontend instead of handing tokens to the browser.
npm install bezzieGet started in 5 minutes
1. Install:
npm install bezzie2. Add a KV namespace to wrangler.toml:
[[kv_namespaces]]
binding = "SESSION_KV"
id = "<your-kv-namespace-id>"3. Add your client secret:
wrangler secret put AUTH0_CLIENT_SECRET4. Wire it up:
import { createBezzie, providers, cloudflareKVAdapter } from 'bezzie'
const auth = createBezzie({
...providers.auth0('your-tenant.auth0.com'),
clientId: 'xxx',
clientSecret: env.AUTH0_CLIENT_SECRET,
adapter: cloudflareKVAdapter(env.SESSION_KV),
baseUrl: 'https://app.yourproject.com',
})
app.route('/auth', auth.routes())
app.use('/api/*', auth.middleware())5. Protect a route:
app.get('/api/me', (c) => c.json(c.var.user))Done. Your app now has BCP212-compliant BFF auth.
Demo
See the full BFF flow in action: bezzie-demo.neilmason.dev
Source: github.com/neilpmas/bezzie-demo
Why
Most OAuth libraries hand tokens directly to the browser. BCP212 says you shouldn't — it's a significant attack surface. Bezzie keeps tokens server-side in Cloudflare KV and gives the browser a session cookie instead.
There's no open source library for this specific combination (BFF OAuth on Cloudflare Workers). The closest alternatives are Duende BFF (.NET) and @auth0/nextjs-auth0 — both tied to specific frameworks.
Usage
import { createBezzie, providers, cloudflareKVAdapter } from 'bezzie'
const auth = createBezzie({
...providers.auth0('your-tenant.auth0.com'),
clientId: 'xxx',
clientSecret: env.AUTH0_CLIENT_SECRET,
audience: 'https://api.yourproject.com',
adapter: cloudflareKVAdapter(env.SESSION_KV),
baseUrl: 'https://app.yourproject.com',
})
// Mount auth routes
app.route('/auth', auth.routes())
// Protect API routes
app.use('/api/*', auth.middleware())This gives you:
| Route | Description |
|---|---|
GET /auth/login |
Redirects to provider, initiates Authorization Code + PKCE flow. Supports returnTo query param for post-login redirect. |
GET /auth/callback |
Exchanges code for tokens, stores session in KV, sets cookie. |
POST /auth/logout |
Clears session, clears cookie, redirects to provider logout. |
Accessing User Identity
After auth.middleware(), downstream handlers can access the user identity and the current access token via c.var:
app.get('/api/me', (c) => {
const user = c.var.user
const token = c.var.accessToken
return c.json({ user })
})Forwarding Upstream
The accessToken on the context is intended for the app to forward to an upstream service (e.g., a Spring Boot API or any other microservice), since Bezzie doesn't mutate request headers directly.
app.all('/api/proxy/*', async (c) => {
const url = new URL(c.req.url)
const target = `https://api.upstream.com${url.pathname}${url.search}`
return fetch(target, {
method: c.req.method,
headers: {
...c.req.header(),
'Authorization': `Bearer ${c.var.accessToken}`
},
body: c.req.raw.body
})
})How It Works
System context
C4Context
title System Context — Bezzie
Person(user, "User", "Browser application user")
System(bezzie, "Cloudflare Worker (bezzie)", "BFF: owns the OAuth flow, issues session cookies to the browser")
System_Ext(idp, "Identity Provider", "Auth0 / Okta / Keycloak / Google — issues tokens")
System_Ext(upstream, "Upstream API", "Your backend — trusts Bearer tokens forwarded by the Worker")
Rel(user, bezzie, "HTTPS requests + session cookie")
Rel(bezzie, idp, "OIDC discovery, token exchange, token refresh")
Rel(bezzie, upstream, "Proxied requests with Authorization: Bearer")
Rel(idp, user, "Redirect back after login")Containers
C4Container
title Container — bezzie deployment
Person(user, "User")
Container(spa, "React SPA", "Cloudflare Pages", "Public landing page + protected dashboard")
Container(worker, "Cloudflare Worker", "Hono + bezzie", "BFF: auth routes + request middleware + token management")
ContainerDb(kv, "Cloudflare KV", "KVNamespace", "Stores sessions and PKCE state")
System_Ext(idp, "Identity Provider", "Auth0 / Okta / Keycloak")
System_Ext(upstream, "Upstream API", "Backend services")
Rel(user, spa, "HTTPS")
Rel(spa, worker, "API calls + __Host-session cookie")
Rel(worker, kv, "Session read / write / delete")
Rel(worker, idp, "OIDC discovery + token exchange + token refresh + JWKS")
Rel(worker, upstream, "Authorization: Bearer {accessToken}")Per-request flow
- Browser sends request to BFF with session cookie
- BFF looks up session in KV, retrieves access token
- BFF validates JWT (via JWKS, using Web Crypto API)
- If expired, BFF uses refresh token to get a new one and updates KV
- BFF forwards request upstream with
Authorization: Bearer <token>
Adapters
Bezzie supports multiple session storage backends:
Cloudflare KV
Recommended for production on Cloudflare Workers.
import { cloudflareKVAdapter } from 'bezzie'
// ...
adapter: cloudflareKVAdapter(env.SESSION_KV)Redis (Upstash)
Good for cross-region session consistency. Works with Upstash Redis (recommended for Cloudflare Workers) and any Redis client with get/set/del methods.
import { redisAdapter } from 'bezzie'
import { Redis } from '@upstash/redis/cloudflare'
adapter: redisAdapter(new Redis({
url: env.UPSTASH_REDIS_REST_URL,
token: env.UPSTASH_REDIS_REST_TOKEN,
}))Memory
Useful for local development and testing. Do not use in production.
import { MemoryAdapter } from 'bezzie'
// ...
adapter: new MemoryAdapter()Configuration
| Option | Type | Description |
|---|---|---|
issuer |
string |
Your OIDC provider issuer URL (e.g. https://tenant.auth0.com) |
clientId |
string |
OAuth client ID |
clientSecret |
string |
OAuth client secret — keep in Workers secrets |
audience |
string |
API audience identifier |
adapter |
SessionAdapter |
Session adapter (e.g. cloudflareKVAdapter(env.SESSION_KV)) |
baseUrl |
string |
Base URL of your application (used for callback and redirects) |
providerOverrides |
object |
Hard overrides for specific provider values (logoutUrl, tokenEndpoint) |
Cloudflare Setup
Add a KV namespace to your wrangler.toml:
[[kv_namespaces]]
binding = "SESSION_KV"
id = "<your-kv-namespace-id>"Add your client secret as a Workers secret:
wrangler secret put AUTH0_CLIENT_SECRETAlternatives
| Bezzie | Auth.js (NextAuth) | Lucia | Roll your own (oauth4webapi) | |
|---|---|---|---|---|
| BFF pattern (tokens never in browser) | ✅ | ✅ (some adapters) | ❌ | You decide |
| Cloudflare Workers native | ✅ | ⚠️ Edge adapter | ⚠️ | ✅ |
| Hono integration | ✅ | ❌ | ❌ | ✅ |
| OIDC discovery | ✅ | ✅ | ❌ | ✅ |
| Token refresh | ✅ | ✅ | Manual | Manual |
| Pluggable session storage | ✅ (KV, Redis, Memory) | ✅ | ✅ | Manual |
| Zero Node.js deps | ✅ | ❌ | ✅ | ✅ |
Stack
| Component | Choice |
|---|---|
| Runtime | Cloudflare Workers |
| Router | Hono |
| OAuth | oauth4webapi (spec-compliant, no Node.js deps) |
| Session storage | Cloudflare KV |
Status
v0.1.0 — pre-release
Changelog
See CHANGELOG.md for release history.
License
MIT