https://github.com/threepointone/cloudflared-login-worker.git
A Cloudflare Worker implementation of the cloudflared access login flow, allowing web applications to authenticate users via Cloudflare Access.
This project replicates the authentication flow used by cloudflared (the Cloudflare Tunnel client) but runs entirely in a Cloudflare Worker + browser. It allows your web application to:
The authentication flow uses Cloudflare's token transfer service with end-to-end encryption. The private key never leaves the browser.
βββββββββββ βββββββββββ ββββββββββββββββ βββββββββββββββββββ
β Browser β β Worker β β Access Login β β Transfer Serviceβ
ββββββ¬βββββ ββββββ¬βββββ βββββββββ¬βββββββ βββββββββββ¬ββββββββ
β β β β
β 1. Generate keypair locally β β
β (private key stays in browser)β β
β β β β
β 2. POST /api/login β β
β {appUrl, publicKey} β β
βββββββββββββββ>β β β
β β 3. Get app info, β β
β β build login URL β
β {loginUrl} β β β
β<βββββββββββββββ β β
β β β β
β 4. Open popup, user logs in β β
βββββββββββββββββββββββββββββββββββ> β
β β β β
β β β 5. Store encrypted β
β β β tokens β
β β ββββββββββββββββββββββ>β
β β β β
β 6. Popup closes (auto) β β
β<ββββββββββββββββββββββββββββββββββ β
β β β β
β 7. GET /api/transfer?publicKey=..β β
βββββββββββββββ>β β β
β β 8. Proxy request β β
β ββββββββββββββββββββββββββββββββββββββββββ>
β {encrypted} β {encrypted} β β
β<βββββββββββββββ<βββββββββββββββββββββββββββββββββββββββββ
β β β β
β 9. Decrypt locally with β β
β private key (in browser) β β
The token transfer uses NaCl Box encryption (Curve25519 + XSalsa20-Poly1305):
βββ src/
β βββ login.ts # Server-side helpers (app info, login URL, transfer proxy)
β βββ server.ts # Worker API endpoints
β βββ app.tsx # React frontend (keypair generation, decryption)
β βββ client.tsx # React entry point
βββ wrangler.jsonc # Worker configuration
βββ package.json
POST /api/loginGet app info and build login URL. The browser provides the public key.
Request:
{
"appUrl": "https://your-access-protected-app.example.com",
"publicKey": "base64url-encoded-public-key",
"autoClose": true
}
Response:
{
"loginUrl": "https://your-app.example.com/cdn-cgi/access/cli?token=...",
"appInfo": {
"authDomain": "your-org.cloudflareaccess.com",
"appAUD": "4dfeff11338191c2342efc3918871896...",
"appDomain": "your-app.example.com"
}
}
GET /api/transfer?publicKey=<key>&fedramp=<bool>Proxy to the transfer service (CORS workaround). Returns encrypted data for the browser to decrypt.
Response (202 - Pending):
{
"status": "pending",
"message": "Token not ready yet"
}
Response (200 - Success):
{
"encryptedData": "base64-encoded-encrypted-data",
"servicePublicKey": "base64url-encoded-service-public-key"
}
GET /api/app-info?appUrl=<url>Get information about an Access-protected application.
Response:
{
"authDomain": "your-org.cloudflareaccess.com",
"appAUD": "4dfeff11338191c2342efc3918871896...",
"appDomain": "your-app.example.com"
}
npm install
npm start
npm run deploy
import nacl from "tweetnacl";
// Helper functions
function encodeBase64url(bytes: Uint8Array): string {
const base64 = btoa(String.fromCharCode(...bytes));
return base64.replace(/\+/g, "-").replace(/\//g, "_");
}
function decodeBase64(str: string): Uint8Array {
const binary = atob(str);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function decodeBase64url(str: string): Uint8Array {
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4) base64 += "=";
return decodeBase64(base64);
}
// 1. Generate keypair locally
const keyPair = nacl.box.keyPair();
const publicKey = encodeBase64url(keyPair.publicKey);
// 2. Get login URL from server
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
appUrl: "https://protected-app.example.com",
publicKey,
}),
});
const { loginUrl } = await response.json();
// 3. Open login in popup
const popup = window.open(loginUrl, "login", "width=500,height=600");
// 4. Poll for encrypted tokens
const pollForTokens = async (): Promise<{ app_token: string; org_token: string }> => {
const res = await fetch(`/api/transfer?publicKey=${encodeURIComponent(publicKey)}`);
if (res.status === 200) {
const { encryptedData, servicePublicKey } = await res.json();
// 5. Decrypt locally
const data = decodeBase64(encryptedData);
const nonce = data.slice(0, 24);
const ciphertext = data.slice(24);
const svcPubKey = decodeBase64url(servicePublicKey);
const decrypted = nacl.box.open(ciphertext, nonce, svcPubKey, keyPair.secretKey);
if (!decrypted) throw new Error("Decryption failed");
return JSON.parse(new TextDecoder().decode(decrypted));
}
if (res.status === 202) {
await new Promise((r) => setTimeout(r, 1000));
return pollForTokens();
}
throw new Error("Login failed");
};
const tokens = await pollForTokens();
console.log("App Token:", tokens.app_token);
console.log("Org Token:", tokens.org_token);
cf-access-token header for requests to that application.