422 lines
10 KiB
Markdown
422 lines
10 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# Gehe zu https://dash.cloudflare.com
|
|
# Registriere dich kostenfrei
|
|
# Du brauchst keine Domain für Workers!
|
|
```
|
|
|
|
### 2. Wrangler installieren (CLI Tool)
|
|
|
|
```bash
|
|
npm install -D wrangler
|
|
npx wrangler login
|
|
```
|
|
|
|
### 3. Projekt initialisieren
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```javascript
|
|
/**
|
|
* 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:
|
|
|
|
```toml
|
|
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`:
|
|
|
|
```javascript
|
|
/**
|
|
* 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
|
|
|
|
```bash
|
|
npx wrangler deploy workers/gog-auth.js --name whattoplay-gog
|
|
npx wrangler deploy workers/blizzard-auth.js --name whattoplay-blizzard
|
|
```
|
|
|
|
### 2. Custom Domain (optional)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
# Logs anschauen
|
|
npx wrangler tail whattoplay-gog
|
|
|
|
# Local testen
|
|
npx wrangler dev workers/gog-auth.js
|
|
# Öffne dann: http://localhost:8787/gog/authorize
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Links
|
|
|
|
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
|
|
- [Wrangler CLI Guide](https://developers.cloudflare.com/workers/wrangler/)
|
|
- [KV Store Reference](https://developers.cloudflare.com/workers/runtime-apis/kv/)
|
|
- [GOG OAuth Docs](https://gogapidocs.readthedocs.io/)
|
|
- [Blizzard OAuth Docs](https://develop.battle.net/documentation/guides/using-oauth)
|