source: Klonkt/src/config/google.js@ 52215bc

main
Last change on this file since 52215bc was 9e27d64, checked in by roboburr <roboburr@…>, 3 months ago

auth: password admin + per-instance Google for listeners (no broker)

Robin's choice: every self-hoster has their own password admin account,
and can optionally let listeners log in to comment using their OWN Google
client. No central broker (that would tie every customer site to Robin's
Google Cloud -> systemic risk on abuse).

  • Admin = username/password (bcrypt). First-time setup via /auth/register (only when there are 0 users); closed afterwards. No public registration.
  • Forgot password: /auth/reset-request -> email (if SMTP configured) with reset link; CLI break-glass npm run reset-admin always works (no email needed).
  • Change password (logged in) restored in /account.
  • Google = per-instance own credentials, OPTIONAL, listeners only -> always role member, never admin (god/admin email is rejected; google_sub mismatch too).
  • config/google.js back to direct Google OAuth; config/mailer.js new (nodemailer).
  • jose removed from deps; nodemailer added.

Security review (workflow) incorporated:

  • Reset token no longer in production logs (dev only).
  • Reset link from PUBLIC_BASE_URL instead of X-Forwarded-Host (host poisoning).
  • Reset tokens stored SHA-256-hashed in the DB.
  • Same-origin check on all state-modifying POSTs (CSRF layer on top of sameSite-lax).
  • Login always runs one bcrypt comparison (no timing enumeration).

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.1 KB
Line 
1// Google OAuth2 voor LUISTERAARS (reageren). Per-instance: de self-hoster zet
2// z'n EIGEN Google-client in .env. Zo hangt elke site aan z'n eigen Google Cloud
3// project — geen centrale afhankelijkheid, geen gedeelde aansprakelijkheid.
4//
5// Optioneel: staat dit niet ingesteld, dan is er simpelweg geen "Login met
6// Google"-knop en blijft de rest van de site werken. Google-login geeft NOOIT
7// beheerrechten — beheer gaat via het wachtwoord-account.
8//
9// Config via env:
10// GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
11// GOOGLE_REDIRECT_URI = https://<dit-domein>/auth/google/callback
12
13const CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
14const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
15const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || '';
16
17const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
18const TOKEN_URL = 'https://oauth2.googleapis.com/token';
19const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
20
21export function googleConfigured() {
22 return !!(CLIENT_ID && CLIENT_SECRET && REDIRECT_URI);
23}
24
25export function authorizeUrl(state) {
26 const p = new URLSearchParams({
27 client_id: CLIENT_ID,
28 redirect_uri: REDIRECT_URI,
29 response_type: 'code',
30 scope: 'openid email profile',
31 state,
32 access_type: 'online',
33 prompt: 'select_account',
34 });
35 return `${AUTH_URL}?${p.toString()}`;
36}
37
38export async function exchangeCode(code) {
39 const body = new URLSearchParams({
40 code,
41 client_id: CLIENT_ID,
42 client_secret: CLIENT_SECRET,
43 redirect_uri: REDIRECT_URI,
44 grant_type: 'authorization_code',
45 });
46 const r = await fetch(TOKEN_URL, {
47 method: 'POST',
48 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
49 body,
50 });
51 if (!r.ok) throw new Error(`Google token-exchange faalde: ${r.status}`);
52 return r.json(); // { access_token, id_token, ... }
53}
54
55// Returns { sub, email, email_verified, name, picture }.
56export async function fetchUserinfo(accessToken) {
57 const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
58 if (!r.ok) throw new Error(`Google userinfo faalde: ${r.status}`);
59 return r.json();
60}
Note: See TracBrowser for help on using the repository browser.