Files
whattoplay/docs/CLOUDFLARE-WORKERS-SETUP.md
2026-03-01 12:03:42 +01:00

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