10 KiB
10 KiB
Cloudflare Workers - Serverless OAuth Proxy
Zero Infrastruktur, alles gekapselt - So funktioniert der Proxy für GOG und Blizzard OAuth Flows.
🎯 Überblick
Statt auf einem eigenen Server zu hosten, nutzen wir Cloudflare Workers als serverless FaaS (Function as a Service):
WhatToPlay Frontend Cloudflare Worker GOG/Blizzard API
↓ ↓ ↓
[Settings speichern] → [OAuth Token Exchange] ← [Bearer Token zurück]
[API aufrufen] → [Token validieren]
Vorteile:
- ✅ Keine Server zu verwalten
- ✅ Kein Backend-Hosting nötig
- ✅ Client Secrets geschützt (Server-Side)
- ✅ Kostenlos bis 100.000 Anfragen/Tag
- ✅ Überall deployed (weltweit verteilt)
- ✅ Automatische CORS-Konfiguration
📋 Setup Anleitung
1. Cloudflare Account erstellen
# Gehe zu https://dash.cloudflare.com
# Registriere dich kostenfrei
# Du brauchst keine Domain für Workers!
2. Wrangler installieren (CLI Tool)
npm install -D wrangler
npx wrangler login
3. Projekt initialisieren
cd whattoplay
npx wrangler init workers
# oder für bestehendes Projekt:
# npx wrangler init whattoplay-oauth --type javascript
🔐 GOG OAuth Worker
Create workers/gog-auth.js:
/**
* GOG OAuth Proxy for WhatToPlay
* Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/gog/callback
*/
const GOG_CLIENT_ID = "your_client_id";
const GOG_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store (nicht in Code!)
const GOG_REDIRECT_URI =
"https://whattoplay-oauth.your-domain.workers.dev/gog/callback";
export default {
async fetch(request) {
const url = new URL(request.url);
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "https://whattoplay.local",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
// Preflight
if (request.method === "OPTIONS") {
return new Response(null, { headers });
}
// 1. Initiiere OAuth Flow
if (url.pathname === "/gog/authorize") {
const authUrl = new URL("https://auth.gog.com/auth");
authUrl.searchParams.append("client_id", GOG_CLIENT_ID);
authUrl.searchParams.append("redirect_uri", GOG_REDIRECT_URI);
authUrl.searchParams.append("response_type", "code");
authUrl.searchParams.append("layout", "client2");
return new Response(null, {
status: 302,
headers: { Location: authUrl.toString() },
});
}
// 2. Callback Handler
if (url.pathname === "/gog/callback") {
const code = url.searchParams.get("code");
if (!code) {
return new Response("Missing authorization code", {
status: 400,
});
}
try {
// Token Exchange (Server-Side!)
const tokenResponse = await fetch("https://auth.gog.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: GOG_CLIENT_ID,
client_secret: GOG_CLIENT_SECRET, // 🔒 Sicher!
grant_type: "authorization_code",
code: code,
redirect_uri: GOG_REDIRECT_URI,
}),
});
const tokenData = await tokenResponse.json();
// Redirect zurück zur App mit Token
const appRedirect = `https://whattoplay.local/#/settings?gog_token=${tokenData.access_token}&gog_user=${tokenData.user_id}`;
return new Response(null, {
status: 302,
headers: { Location: appRedirect },
});
} catch (error) {
return new Response(`Token Error: ${error.message}`, {
status: 500,
});
}
}
// 3. Token Validation
if (url.pathname === "/gog/validate") {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return new Response("Missing Authorization", {
status: 401,
});
}
const token = authHeader.replace("Bearer ", "");
try {
const response = await fetch(
"https://galaxy-library.gog.com/users/me",
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (response.ok) {
const data = await response.json();
return new Response(JSON.stringify({ valid: true, user: data }), {
headers,
});
}
return new Response(JSON.stringify({ valid: false }), {
headers,
});
} catch (error) {
return new Response(
JSON.stringify({ valid: false, error: error.message }),
{
headers,
},
);
}
}
return new Response("Not Found", { status: 404 });
},
};
wrangler.toml Config:
name = "whattoplay-oauth"
main = "src/index.js"
compatibility_date = "2024-01-01"
# KV Store für Secrets
[[kv_namespaces]]
binding = "SECRETS"
id = "your_kv_namespace_id"
preview_id = "your_preview_kv_id"
# Environment Variables (Secrets!)
[env.production]
vars = { ENVIRONMENT = "production" }
[env.production.secrets]
GOG_CLIENT_SECRET = "your_client_secret"
BLIZZARD_CLIENT_SECRET = "your_client_secret"
🎮 Blizzard OAuth Worker
Create workers/blizzard-auth.js:
/**
* Blizzard OAuth Proxy for WhatToPlay
* Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback
*/
const BLIZZARD_CLIENT_ID = "your_client_id";
const BLIZZARD_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store!
const BLIZZARD_REDIRECT_URI =
"https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback";
export default {
async fetch(request) {
const url = new URL(request.url);
const headers = {
"Access-Control-Allow-Origin": "https://whattoplay.local",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
if (request.method === "OPTIONS") {
return new Response(null, { headers });
}
// 1. Authorize
if (url.pathname === "/blizzard/authorize") {
const state = crypto.randomUUID();
const authUrl = new URL("https://oauth.battle.net/authorize");
authUrl.searchParams.append("client_id", BLIZZARD_CLIENT_ID);
authUrl.searchParams.append("redirect_uri", BLIZZARD_REDIRECT_URI);
authUrl.searchParams.append("response_type", "code");
authUrl.searchParams.append("state", state);
return new Response(null, {
status: 302,
headers: { Location: authUrl.toString() },
});
}
// 2. Callback
if (url.pathname === "/blizzard/callback") {
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if (!code) {
return new Response("Missing authorization code", {
status: 400,
});
}
try {
const tokenResponse = await fetch("https://oauth.battle.net/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: BLIZZARD_CLIENT_ID,
client_secret: BLIZZARD_CLIENT_SECRET, // 🔒 Sicher!
grant_type: "authorization_code",
code: code,
redirect_uri: BLIZZARD_REDIRECT_URI,
}),
});
if (!tokenResponse.ok) {
throw new Error(`Token request failed: ${tokenResponse.status}`);
}
const tokenData = await tokenResponse.json();
// Redirect zurück
const appRedirect = `https://whattoplay.local/#/settings?blizzard_token=${tokenData.access_token}`;
return new Response(null, {
status: 302,
headers: { Location: appRedirect },
});
} catch (error) {
return new Response(`Error: ${error.message}`, {
status: 500,
});
}
}
return new Response("Not Found", { status: 404 });
},
};
🚀 Deployment
1. Deploy zu Cloudflare
npx wrangler deploy workers/gog-auth.js --name whattoplay-gog
npx wrangler deploy workers/blizzard-auth.js --name whattoplay-blizzard
2. Custom Domain (optional)
# Wenn du einen Domain hast, verbinde Cloudflare:
# https://dash.cloudflare.com → Workers Routes
# Beispiel:
# Domain: api.whattoplay.com
# Worker: whattoplay-oauth
# Route: api.whattoplay.com/gog/*
3. Secrets hinzufügen
# GOG Secret
echo "your_gog_secret" | npx wrangler secret put GOG_CLIENT_SECRET --name whattoplay-gog
# Blizzard Secret
echo "your_blizzard_secret" | npx wrangler secret put BLIZZARD_CLIENT_SECRET --name whattoplay-blizzard
🔗 Frontend Integration
In SettingsPage.tsx:
// Button für GOG OAuth Login
const handleGogOAuth = () => {
const workerUrl = "https://whattoplay-oauth.workers.dev/gog/authorize";
window.location.href = workerUrl;
};
// Callback mit URL-Parametern
const handleOAuthCallback = () => {
const params = new URLSearchParams(window.location.hash.split("?")[1]);
const token = params.get("gog_token");
const userId = params.get("gog_user");
if (token) {
handleSaveConfig("gog", {
accessToken: token,
userId: userId,
});
// Token ist jetzt gespeichert in localStorage
}
};
📊 Kosten (Februar 2026)
| Service | Free Tier | Kosten |
|---|---|---|
| Cloudflare Workers | 100k req/Tag | $0.50 pro 10M Anfragen |
| KV Store | 3GB Storage | $0.50 pro GB |
| Bandwidth | Unlimited | Keine Zusatzkosten |
Beispiel: 1.000 Users, je 10 Tokens/Monat = 10.000 Anfragen = Kostenlos 🎉
🔒 Security Best Practices
✅ Was wir tun:
- Client Secrets in KV Store (nicht im Code)
- Token Exchange Server-Side
- CORS nur für unsere Domain
- State Parameter für CSRF Protection
- Keine Tokens in URLs speichern (Session nur)
❌ Was wir NICHT tun:
- Client Secrets hardcoden
- Tokens in localStorage ohne Verschlüsselung
- CORS für alle Origins
- Tokens in Browser Console anzeigen
🐛 Debugging
# Logs anschauen
npx wrangler tail whattoplay-gog
# Local testen
npx wrangler dev workers/gog-auth.js
# Öffne dann: http://localhost:8787/gog/authorize