Add "Sign in with Google" to your Worker app. No auth libraries, no frameworks — just the raw OAuth 2.0 flow running at the edge. By the end you'll have a Worker that authenticates users via their Google account and stores sessions in KV.
The flow has 4 steps:
codeThe code → token exchange happens server-side (in your Worker), so your client secret never reaches the browser. The Worker runs over HTTPS automatically — Cloudflare handles the certificate.
http://localhost:8787/auth/callbackhttps://your-worker.your-subdomain.workers.dev/auth/callbackhttps://app.yourdomain.com/auth/callback)You also need to configure the OAuth consent screen (APIs & Services → OAuth consent screen). For testing, set it to "External" with your email as a test user. You can publish it later when ready.
npm create cloudflare@latest -- google-auth-worker
cd google-auth-worker
Store your secrets securely (never in code):
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
Add a KV namespace for sessions:
npx wrangler kv namespace create SESSIONS
Update wrangler.jsonc:
{
"name": "google-auth-worker",
"main": "src/index.js",
"compatibility_date": "2024-01-01",
"kv_namespaces": [
{
"binding": "SESSIONS",
"id": "<your-namespace-id>"
}
]
}
For local development, create a .dev.vars file (git-ignored) with your secrets:
# .dev.vars (add to .gitignore!)
GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
Here's the full implementation — 4 routes handling the entire flow:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const REDIRECT_URI = `${url.origin}/auth/callback`;
// Route: Start OAuth flow
if (url.pathname === "/auth/login") {
const params = new URLSearchParams({
client_id: env.GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: "code",
scope: "openid email profile",
access_type: "offline",
prompt: "consent",
});
return Response.redirect(
`https://accounts.google.com/o/oauth2/v2/auth?${params}`
);
}
// Route: OAuth callback — exchange code for tokens
if (url.pathname === "/auth/callback") {
const code = url.searchParams.get("code");
if (!code) {
return new Response("Missing authorization code", { status: 400 });
}
// Exchange code for tokens
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
client_id: env.GOOGLE_CLIENT_ID,
client_secret: env.GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
}),
});
const tokens = await tokenRes.json();
if (!tokens.access_token) {
return new Response("Token exchange failed", { status: 400 });
}
// Fetch user info from Google
const userRes = await fetch(
"https://www.googleapis.com/oauth2/v2/userinfo",
{ headers: { Authorization: `Bearer ${tokens.access_token}` } }
);
const user = await userRes.json();
// Create a session
const sessionId = crypto.randomUUID();
await env.SESSIONS.put(sessionId, JSON.stringify({
email: user.email,
name: user.name,
picture: user.picture,
created: Date.now(),
}), { expirationTtl: 86400 }); // 24 hours
// Set session cookie and redirect to app
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400`,
},
});
}
// Route: Logout
if (url.pathname === "/auth/logout") {
const session = getSession(request);
if (session) await env.SESSIONS.delete(session);
return new Response(null, {
status: 302,
headers: {
Location: "/",
"Set-Cookie": "session=; Path=/; Max-Age=0",
},
});
}
// Route: Home page — show login state
const session = getSession(request);
const user = session
? JSON.parse(await env.SESSIONS.get(session) || "null")
: null;
if (user) {
return new Response(`
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 0 1rem;">
<h1>Welcome, ${user.name}!</h1>
<p>Email: ${user.email}</p>
<img src="${user.picture}" width="80" style="border-radius: 50%;">
<p><a href="/auth/logout">Sign out</a></p>
</body>
</html>
`, { headers: { "Content-Type": "text/html" } });
}
return new Response(`
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 0 1rem;">
<h1>My App</h1>
<p><a href="/auth/login">Sign in with Google</a></p>
</body>
</html>
`, { headers: { "Content-Type": "text/html" } });
},
};
// Helper: extract session ID from cookie
function getSession(request) {
const cookie = request.headers.get("Cookie") || "";
const match = cookie.match(/session=([^;]+)/);
return match ? match[1] : null;
}
| Route | What It Does |
|---|---|
/auth/login | Redirects user to Google's consent screen |
/auth/callback | Receives the code from Google, exchanges it for tokens, fetches user info, creates session in KV, sets cookie |
/auth/logout | Deletes session from KV, clears cookie |
/ | Shows logged-in user info or login link |
npx wrangler dev
Visit http://localhost:8787 and click "Sign in with Google". You'll be redirected to Google, then back to your local Worker with your profile info displayed.
Make sure http://localhost:8787/auth/callback is in your Google OAuth authorized redirect URIs. Google allows localhost for development without HTTPS.
npx wrangler deploy
After deploying, add your production URL (https://google-auth-worker.<your-account>.workers.dev/auth/callback) to Google's authorized redirect URIs.
expirationTtl auto-deletes sessions after 24 hoursFor a real app, also add: a CSRF state parameter (store in KV, verify on callback), scope restriction to only what you need, and rate limiting on /auth/login to prevent abuse.
| Resource | Usage per Login | Daily Budget (Free) |
|---|---|---|
| Worker requests | 3 (login redirect + callback + home) | 100,000 |
| KV writes | 1 (create session) | 1,000 |
| KV reads | 1 per page load (verify session) | 100,000 |
That's ~1,000 new logins per day and 100,000 authenticated page views — more than enough for personal projects.
Google OAuth 2.0 for Web Server Applications — the official spec. Covers all parameters, scopes, and error handling in detail.