120 lines
4.7 KiB
JavaScript
120 lines
4.7 KiB
JavaScript
/* =====================================================================
|
|
lib/oauth-handlers.js — gemeinsame Start-/Callback-Logik für alle OAuth-Anbieter. 1:1 portiert
|
|
aus functions/_shared/oauth-handlers.js, auf Express (req/res) statt Cloudflare
|
|
Request/Response umgestellt.
|
|
===================================================================== */
|
|
|
|
import {
|
|
erzeugeZufallswert,
|
|
codeChallengeFuer,
|
|
baueAutorisierungsUrl,
|
|
istProviderKonfiguriert,
|
|
tauscheCodeGegenToken,
|
|
holeNutzerprofil,
|
|
} from "./oauth.js";
|
|
import { signCustomerSession, setCustomerCookie, CUSTOMER_SESSION_TAGE } from "./customer-auth.js";
|
|
import { db } from "../db.js";
|
|
|
|
const STATE_COOKIE = "vandiy_oauth_state";
|
|
const VERIFIER_COOKIE = "vandiy_oauth_verifier";
|
|
const LANG_COOKIE = "vandiy_oauth_lang";
|
|
const OAUTH_TEMP_MAXAGE = 600; // Sekunden
|
|
const ERLAUBTE_SPRACHEN = ["", "en", "ch", "fr"];
|
|
|
|
function langPrefix(lang) {
|
|
return ERLAUBTE_SPRACHEN.includes(lang) ? lang : "";
|
|
}
|
|
|
|
function originVon(req) {
|
|
return `${req.protocol}://${req.get("host")}`;
|
|
}
|
|
|
|
/** GET /api/auth/:provider/start */
|
|
export async function starteOAuth(req, res, providerName) {
|
|
const origin = originVon(req);
|
|
const lang = langPrefix(req.query.lang || "");
|
|
|
|
if (!istProviderKonfiguriert(providerName)) {
|
|
return res.redirect(302, `${origin}/${lang ? lang + "/" : ""}konto/?login_error=${providerName}_unconfigured`);
|
|
}
|
|
|
|
const state = erzeugeZufallswert(24);
|
|
const codeVerifier = erzeugeZufallswert(48);
|
|
const codeChallenge = await codeChallengeFuer(codeVerifier);
|
|
const redirectUri = `${origin}/api/auth/${providerName}/callback`;
|
|
const authUrl = baueAutorisierungsUrl(providerName, { redirectUri, state, codeChallenge });
|
|
|
|
const cookiePath = `/api/auth/${providerName}`;
|
|
const cookieOpts = { path: cookiePath, maxAge: OAUTH_TEMP_MAXAGE * 1000, httpOnly: true, secure: true, sameSite: "lax" };
|
|
res.cookie(STATE_COOKIE, state, cookieOpts);
|
|
res.cookie(VERIFIER_COOKIE, codeVerifier, cookieOpts);
|
|
res.cookie(LANG_COOKIE, lang, cookieOpts);
|
|
return res.redirect(302, authUrl);
|
|
}
|
|
|
|
/** GET /api/auth/:provider/callback */
|
|
export async function verarbeiteOAuthCallback(req, res, providerName) {
|
|
const origin = originVon(req);
|
|
const code = req.query.code;
|
|
const state = req.query.state;
|
|
const errorParam = req.query.error;
|
|
const lang = langPrefix(req.cookies?.[LANG_COOKIE] || "");
|
|
const kontoPfad = `${origin}/${lang ? lang + "/" : ""}konto/`;
|
|
const cookiePath = `/api/auth/${providerName}`;
|
|
|
|
function loescheTempCookies() {
|
|
res.clearCookie(STATE_COOKIE, { path: cookiePath });
|
|
res.clearCookie(VERIFIER_COOKIE, { path: cookiePath });
|
|
res.clearCookie(LANG_COOKIE, { path: cookiePath });
|
|
}
|
|
function fehlerRedirect(grund) {
|
|
loescheTempCookies();
|
|
return res.redirect(302, `${kontoPfad}?login_error=${grund}`);
|
|
}
|
|
|
|
if (errorParam) return fehlerRedirect(`${providerName}_denied`);
|
|
if (!code || !state) return fehlerRedirect(`${providerName}_invalid`);
|
|
|
|
const stateCookie = req.cookies?.[STATE_COOKIE];
|
|
const verifierCookie = req.cookies?.[VERIFIER_COOKIE];
|
|
if (!stateCookie || stateCookie !== state || !verifierCookie) {
|
|
return fehlerRedirect(`${providerName}_state_mismatch`);
|
|
}
|
|
|
|
if (!process.env.CUSTOMER_SESSION_SECRET) {
|
|
return fehlerRedirect(`${providerName}_backend_unconfigured`);
|
|
}
|
|
|
|
try {
|
|
const redirectUri = `${origin}/api/auth/${providerName}/callback`;
|
|
const accessToken = await tauscheCodeGegenToken(providerName, { code, redirectUri, codeVerifier: verifierCookie });
|
|
const profil = await holeNutzerprofil(providerName, accessToken);
|
|
|
|
const now = new Date().toISOString();
|
|
const bestehend = db
|
|
.prepare(`SELECT id FROM customers WHERE provider = ? AND provider_user_id = ?`)
|
|
.get(providerName, profil.providerUserId);
|
|
|
|
let customerId;
|
|
if (bestehend) {
|
|
customerId = bestehend.id;
|
|
db.prepare(`UPDATE customers SET email = ?, name = ?, updated_at = ?, last_login_at = ? WHERE id = ?`)
|
|
.run(profil.email, profil.name, now, now, customerId);
|
|
} else {
|
|
const insert = db
|
|
.prepare(
|
|
`INSERT INTO customers (created_at, updated_at, last_login_at, provider, provider_user_id, email, name) VALUES (?,?,?,?,?,?,?)`
|
|
)
|
|
.run(now, now, now, providerName, profil.providerUserId, profil.email, profil.name);
|
|
customerId = insert.lastInsertRowid;
|
|
}
|
|
|
|
const sessionToken = await signCustomerSession(customerId, process.env.CUSTOMER_SESSION_SECRET);
|
|
setCustomerCookie(res, sessionToken, CUSTOMER_SESSION_TAGE * 24 * 60 * 60);
|
|
loescheTempCookies();
|
|
return res.redirect(302, `${origin}/${lang ? lang + "/" : ""}konto/angemeldet/`);
|
|
} catch (err) {
|
|
return fehlerRedirect(`${providerName}_failed`);
|
|
}
|
|
}
|