Node.js/Express-Server als Ersatz für Cloudflare Pages Functions + D1 (läuft auf eigenem Server statt Cloudflare)
Build & Deploy / deploy (push) Waiting to run
Build & Deploy / deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/* =====================================================================
|
||||
lib/oauth.js — echte OAuth-/OpenID-Connect-Anmeldung für Google und "Log in with PayPal".
|
||||
1:1 portiert aus functions/_shared/oauth.js, Zugangsdaten aus process.env statt env.*.
|
||||
===================================================================== */
|
||||
|
||||
function paypalBasis() {
|
||||
return process.env.PAYPAL_ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
|
||||
}
|
||||
function paypalSigninBasis() {
|
||||
return process.env.PAYPAL_ENV === "live" ? "https://www.paypal.com" : "https://www.sandbox.paypal.com";
|
||||
}
|
||||
|
||||
const PROVIDERS = {
|
||||
google: {
|
||||
authorizeUrl: () => "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenUrl: () => "https://oauth2.googleapis.com/token",
|
||||
userinfoUrl: () => "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
scope: "openid email profile",
|
||||
clientIdEnvKey: "GOOGLE_CLIENT_ID",
|
||||
clientSecretEnvKey: "GOOGLE_CLIENT_SECRET",
|
||||
tokenAuthStyle: "body",
|
||||
parseUserinfo: (data) => ({ providerUserId: data.sub, email: data.email, name: data.name || null }),
|
||||
},
|
||||
paypal: {
|
||||
authorizeUrl: () => `${paypalSigninBasis()}/signin/authorize`,
|
||||
tokenUrl: () => `${paypalBasis()}/v1/oauth2/token`,
|
||||
userinfoUrl: () => `${paypalBasis()}/v1/identity/openidconnect/userinfo/?schema=openid`,
|
||||
scope: "openid email profile",
|
||||
clientIdEnvKey: "PAYPAL_CLIENT_ID",
|
||||
clientSecretEnvKey: "PAYPAL_CLIENT_SECRET",
|
||||
tokenAuthStyle: "basic",
|
||||
parseUserinfo: (data) => ({ providerUserId: data.user_id || data.payer_id, email: data.email, name: data.name || null }),
|
||||
},
|
||||
};
|
||||
|
||||
export function provider(name) {
|
||||
const p = PROVIDERS[name];
|
||||
if (!p) throw new Error(`Unbekannter OAuth-Anbieter: ${name}`);
|
||||
return p;
|
||||
}
|
||||
|
||||
export function istProviderKonfiguriert(name) {
|
||||
const p = provider(name);
|
||||
return !!(process.env[p.clientIdEnvKey] && process.env[p.clientSecretEnvKey]);
|
||||
}
|
||||
|
||||
export function baueAutorisierungsUrl(name, { redirectUri, state, codeChallenge }) {
|
||||
const p = provider(name);
|
||||
const clientId = process.env[p.clientIdEnvKey];
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
response_type: "code",
|
||||
scope: p.scope,
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
});
|
||||
if (name === "google") params.set("prompt", "select_account");
|
||||
return `${p.authorizeUrl()}?${params.toString()}`;
|
||||
}
|
||||
|
||||
export async function tauscheCodeGegenToken(name, { code, redirectUri, codeVerifier }) {
|
||||
const p = provider(name);
|
||||
const clientId = process.env[p.clientIdEnvKey];
|
||||
const clientSecret = process.env[p.clientSecretEnvKey];
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
});
|
||||
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
||||
if (p.tokenAuthStyle === "basic") {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
||||
} else {
|
||||
body.set("client_id", clientId);
|
||||
body.set("client_secret", clientSecret);
|
||||
}
|
||||
const res = await fetch(p.tokenUrl(), { method: "POST", headers, body: body.toString() });
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Token-Austausch bei ${name} fehlgeschlagen (Status ${res.status}): ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.access_token) throw new Error(`${name} hat kein Zugangstoken geliefert.`);
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
export async function holeNutzerprofil(name, accessToken) {
|
||||
const p = provider(name);
|
||||
const res = await fetch(p.userinfoUrl(), { headers: { Authorization: `Bearer ${accessToken}` } });
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`Profilabruf bei ${name} fehlgeschlagen (Status ${res.status}): ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const profil = p.parseUserinfo(data);
|
||||
if (!profil.providerUserId || !profil.email) {
|
||||
throw new Error(`${name} hat kein vollständiges Profil geliefert (E-Mail/ID fehlt).`);
|
||||
}
|
||||
return profil;
|
||||
}
|
||||
|
||||
function b64url(bytes) {
|
||||
let bin = "";
|
||||
bytes.forEach((b) => (bin += String.fromCharCode(b)));
|
||||
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
export function erzeugeZufallswert(laenge = 32) {
|
||||
const bytes = new Uint8Array(laenge);
|
||||
crypto.getRandomValues(bytes);
|
||||
return b64url(bytes);
|
||||
}
|
||||
|
||||
export async function codeChallengeFuer(codeVerifier) {
|
||||
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
|
||||
return b64url(new Uint8Array(hash));
|
||||
}
|
||||
Reference in New Issue
Block a user