Stateless Edge Auth: PKCE OAuth, HttpOnly Cookies, and a VS Code Handshake on Cloudflare Functions
TLDR
When building web developer tools on a zero-persistent-server architecture, we had to manage auth across three distinct surfaces: a browser modal, a standalone landing page, and a native VS Code webview extension. By pairing stateless Cloudflare Functions with Supabase as an external identity provider, we enforced short-lived JWTs, HttpOnly cookie storage for web sessions, OS Keychain storage for the editor extension, and a polling handshake with temporary AES-GCM encryption.| Surface | Session Storage Mechanism | Auth Protocol / Pattern | Key Expiration & Security Boundary |
|---|---|---|---|
| Browser Modal | HttpOnly gx_session Cookie | PKCE OAuth + BroadcastChannel | 1-Hour JWT max; 600ms transient localStorage handoff |
| Standalone Page | HttpOnly gx_session Cookie | Direct OAuth / Password grant | Refresh token stripped; valid only while tab is active |
| VS Code Extension | OS Keychain (context.secrets) | Polling Handshake via AES-GCM | Encrypted via ephemeral HMAC key; auto-purged on delivery |
Zero-server architectures require edge-based auth boundaries
When we set out to build our developer tool suite, we ran straight into a strict architectural constraint: zero persistent backend servers. We did not want a database cluster sitting around statefully tracking user session tokens on every HTTP request. Everything had to run inside stateless edge workers (Cloudflare Functions) paired with Supabase as an external identity provider.
This constraint immediately eliminated standard auth patterns. We could not maintain server-side session IDs in a database, nor could we run server-side refresh token rotation with real-time revocation lists. Every single session had to rely either on stateless JWT verification or an external ephemeral store.
The main trade-off was capping session lifetimes at the JWT expiration limit, exactly one hour. For a developer tool, that is completely reasonable, but orchestrating the handoff smoothly across browser modals, standalone tabs, and native VS Code webview panels took careful wiring.
Multi-surface client environments introduce token exposure and state loss
Supporting three separate client surfaces created distinct edge cases that could easily compromise session security if handled carelessly:
- Browser modal state collisions: Standard OAuth redirects reload the active page, which would completely destroy the developer's in-memory workbench state (such as unsaved canvas regions or active table edits).
- Refresh token exposure: Storing long-lived refresh tokens in browser
localStorageleaves the system vulnerable to token exfiltration if any third-party script or XSS payload manages to execute. - VS Code webview isolation: Native VS Code webview panels cannot accept standard HTTP redirects from OAuth providers without breaking the internal extension host state.
Decoupled edge handlers isolate credentials by client platform
We solved these issues by creating explicit boundaries for each surface while sharing a single stateless edge endpoint:
Browser modal and ephemeral handoff
For password sign-ins,signInWithPassword() returns a session. The modal sends a POST to /api/auth/session containing { action: 'set', access_token, expires_in } to establish the HttpOnly cookie, immediately purging Supabase keys from localStorage. The token sits in localStorage for roughly 600ms during handoff.
For OAuth, we pass skipBrowserRedirect: true to open the provider in a popup window, preserving the main tab's state. Once authenticated, the popup sends a gx:auth-complete event via BroadcastChannel to the main window.
Main Window OAuth Popup Window OAuth Provider CF Edge Function
| | | |
|--- 1. Open popup --------->| | |
| (skipBrowserRedirect) | | |
| |--- 2. Authenticate --->| |
| |<-- 3. Access token ----| |
|<-- 4. Broadcast -----------| | |
| "gx:auth-complete" | | |
| |--- 5. Close popup ---->| (close) |
|--- 6. POST /session (token) ------------------------------------------------>|
|<-- 7. Set-Cookie: gx_session (HttpOnly) -------------------------------------|
|--- 8. Evict localStorage --| | |
async function handleOAuthReturn() {
if (!oauthPending) return;
oauthPending = false;
const token = readStoredToken(); // Reads transient Supabase localStorage
if (!token) return;
// Evict immediately: HttpOnly cookie is the true session for (let i = localStorage.length - 1; i >= 0; i--) { const k = localStorage.key(i); if (k?.startsWith('sb-') && k.endsWith('-auth-token')) localStorage.removeItem(k); } await onSignInSuccess(token); }
Standalone login page
For direct logins, we strip therefresh_token field from the session object before persisting anything. If an access token is compromised, it naturally expires in one hour; by refusing to store the refresh token on the client, we ensure a stolen credential cannot grant indefinite access.
VS Code extension polling handshake
To bridge VS Code and the web browser securely, we built a zero-knowledge polling protocol:- The extension generates a unique
stateUUID and a 32-byte hex secret. - The extension sends a
POSTto/auth/vscodewithAuthorization: Bearer <secret>. The edge worker creates a temporary session row with a 5-minute TTL storing the hashed secret. - The extension opens the browser login page with
?state=<state>&from=vscode. - Upon successful login, the web page encrypts the access token using AES-GCM with a key derived from
HMAC-SHA256(SERVICE_ROLE_KEY, state)and writes it to the session row. - The extension polls
/auth/vscode-poll?state=<state>with its bearer secret every 5 seconds. - The edge function decrypts the payload, returns the token, and deletes the row instantly.
- The extension saves the token inside the OS keychain via
context.secrets.store().
VS Code Extension Cloudflare Edge Worker Web Browser Login Page
| | |
|--- 1. POST /auth/vscode ----->| |
| (Bearer secret) | |
| |--- Store hashed secret -------|
| | (5-min TTL) |
|--- 2. Open login page (?state=UUID) ------------------------->|
| |<-- 3. Auth & write GCM token -|
| | |
|=== [Loop Every 5 seconds] ====================================|
| | |
|--- 4. GET /auth/vscode-poll ->| |
| (state, secret) | |
|===============================================================|
| | |
|<-- 5. Decrypted access token -| |
| & delete row | |
|--- 6. Save to OS Keychain ----| |
Server-side edge endpoints enforce HttpOnly scopes and PKCE validation
All web surfaces converge on a single edge function endpoint that sets thegx_session cookie:
// Set Session
const cookie = gx_session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge};
// Clear Session const cookie = gx_session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0;
Every auth request also explicitly enforces flowType: 'pkce'. Following RFC 7636, the client generates a code_verifier and hashes it into a code_challenge. Even if an attacker intercepts the authorization code in flight, it is useless without the verifier residing inside browser memory.
Rule of thumb: Never rely on client-side cleanup for HttpOnly cookies. Because JavaScript cannot read HttpOnly headers, explicit logouts must always execute a server-side roundtrip with Max-Age=0 to invalidate the session cookie.