Self-hosted authentication for Cloudflare Workers.
- Cross-subdomain SSO
- OAuth login (GitHub)
- Session-based auth (no JWTs)
- Session hijack protection
- Built using D1 + KV
- Audit logging
User
│
▼
App Worker ── Service Binding ──► Derbent Auth Worker
│ │
│ ├── KV (sessions)
│ └── D1 (users + audit logs)
▼
Cloudflare Cache
You can deploy Derbent using the automated 1-click deploy button or manually via the CLI.
The button above will automatically clone this repository to your GitHub account, provision your Cloudflare resources (KV, D1, Queues), run the required database migrations, and safely prompt you for the necessary environment variables.
Once deployed, clone your new repository locally and proceed to Step 3: Registering Your Apps below to configure your allowed applications.
git clone https://github.com/medreseli/derbent.git
cd derbent
npm installYou need to create your own Cloudflare resources for this instance:
- KV:
npx wrangler kv namespace create KV - D1:
npx wrangler d1 create db-derbent - Queue:
npx wrangler queues create derbent-email-queue - Paste the generated IDs into your
wrangler.jsonc.
Derbent uses a strict whitelist to determine which apps are allowed to authenticate.
Open src/config/apps.ts and add your applications (e.g., geveze, namedar) to the ALLOWED_APPS array and REGISTERED_APPS object along with their production and development URLs.
Create a .dev.vars file for development. For production, use wrangler secret.
# APP_ENV: 'development' or 'production'
APP_ENV=development
LOG_LEVEL=debug
APP_NAME=Derbent Auth
COOKIE_DOMAIN=localhost
BASE_URL=http://localhost:8787
RESEND_API_KEY=re_your_api_key_here
RESEND_DOMAIN=your-verified-domain.com
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secretNote for GitHub Login: You should create 2 OAuth apps in GitHub. One for local testing and the other for production. The Authorization callback URL format is https://<your-domain>/auth/github/callback. When you deploy your app, do not forget to use the production OAuth app's client ID and secret.
Prepare database:
npx wrangler d1 migrations apply db-derbent --localRun locally:
npm run devDerbent acts as a sidecar for your other services. Use Service Bindings to connect them without touching the public internet.
In your consuming app's wrangler.jsonc, add the service binding:
Every consuming app (e.g., geveze, namedar) should use the following pattern to verify users.
Important: You must pass the end-user's IP and User-Agent using the Derbent-Client-* headers to maintain audit logging and session hijack protection.
export async function verifyWithDerbent(c: Context, appId: string) {
const cache = caches.default;
const cookie = c.req.header('Cookie') || '';
// If the cookie is empty, we can skip the fetch entirely to save CPU
if (!cookie) {
return c.json({ error: 'Unauthorized' }, 401);
}
const clientIp = c.req.header('cf-connecting-ip') || '127.0.0.1';
const clientUa = c.req.header('user-agent') || 'unknown';
// SECURITY: Cloudflare Cache API ignores the 'Vary: Cookie' header.
// To prevent cross-session leaking, we create a unique cache key by hashing the cookie.
const encoder = new TextEncoder();
const data = encoder.encode(cookie);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const cookieHash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// PERFORMANCE: Use the consuming Worker's actual hostname to prevent DNS lookup penalties in the Cache API.
const currentUrl = new URL(c.req.url);
const cacheUrl = new URL(`${currentUrl.origin}/_internal_auth_cache`);
cacheUrl.searchParams.set('app_id', appId);
cacheUrl.searchParams.set('cookie_hash', cookieHash);
const cacheKey = new Request(cacheUrl.toString());
let response = await cache.match(cacheKey);
if (!response) {
// The actual request to Derbent via Service Binding.
const fetchReq = new Request(`https://auth.internal/internal/verify?app_id=${appId}`, {
headers: {
Cookie: cookie,
'Derbent-Client-IP': clientIp,
'Derbent-Client-UA': clientUa,
},
});
response = await c.env.DERBENT_SERVICE.fetch(fetchReq);
if (response.ok) {
// Cache the response against our unique cacheKey
await cache.put(cacheKey, response.clone());
}
}
return response;
}Note: Derbent sends Vary: Cookie and Cache-Control: private, max-age=60 by default.
npm run test- No JWTs: Opaque tokens only.
- Stateful: Session data stored in Cloudflare KV.
- Root Domain Cookies: Scoped to
.yourdomain.comfor cross-subdomain SSO. - Isolation: Supports both Global
ssoaccounts and app-specific accounts.
- SSO Priority: Once an
ssoaccount exists for an email, app-specific accounts for that email cannot be created. - Rate Limiting: Protects against brute force.
- Audit Logging: Every action is recorded in the D1
audit_logstable. - Hijack Prevention: Sessions are bound to
User-AgentandIP.
Derbent works well for:
• SaaS apps on Cloudflare Workers
• Multi-subdomain applications
• Edge-native APIs
• Self-hosted authentication systems
• Replacing Auth0 for Workers projects
{ "services": [ { "binding": "DERBENT_SERVICE", "service": "derbent", // Name of the Derbent Worker }, ], }