Changeset 9e27d64 in Klonkt for src/config/google.js
- Timestamp:
- 06/14/2026 07:31:23 AM (3 months ago)
- Branches:
- main
- Children:
- 6351545
- Parents:
- 32cc601
- File:
-
- 1 edited
-
src/config/google.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/google.js
r32cc601 r9e27d64 1 // Google-login via de centrale Klonkt-broker (license.klonkt.com). 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. 2 4 // 3 // Deze instance praat NOOIT zelf met Google. De broker doet de OAuth-dans met één 4 // centrale Google-client en stuurt een kortlevend, gesigneerd identity-token terug; 5 // dat verifiëren we offline tegen de broker-pubkey. Zo hoeft geen enkele self-host 6 // een eigen Google-client aan te maken. 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. 7 8 // 8 9 // Config via env: 9 // KLONKT_BROKER_URL = https://license.klonkt.com 10 // SITE_ORIGIN = het eigen publieke origin (bv https://roboburr.com) — 11 // bepaalt de callback + de audience die we eisen. 12 // ADMIN_EMAIL = de Google-mail die owner/admin (god) is op deze instance. 10 // GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET 11 // GOOGLE_REDIRECT_URI = https://<dit-domein>/auth/google/callback 13 12 14 import { importSPKI, jwtVerify } from 'jose'; 13 const CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''; 14 const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''; 15 const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || ''; 15 16 16 const ALG = 'EdDSA'; 17 const ISSUER = 'klonkt-license'; 17 const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; 18 const TOKEN_URL = 'https://oauth2.googleapis.com/token'; 19 const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo'; 18 20 19 const BROKER_URL = (process.env.KLONKT_BROKER_URL || '').replace(/\/$/, ''); 20 const SITE_ORIGIN = (process.env.SITE_ORIGIN || '').replace(/\/$/, ''); 21 22 export function brokerConfigured() { 23 return !!(BROKER_URL && SITE_ORIGIN); 21 export function googleConfigured() { 22 return !!(CLIENT_ID && CLIENT_SECRET && REDIRECT_URI); 24 23 } 25 24 26 // Waar de broker naartoe terugstuurt (moet in de broker-allowlist staan). 27 export function callbackUrl() { 28 return `${SITE_ORIGIN}/auth/google/callback`; 25 export 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()}`; 29 36 } 30 37 31 export function brokerStartUrl(istate) { 32 const p = new URLSearchParams({ return: callbackUrl(), istate }); 33 return `${BROKER_URL}/auth/google/start?${p.toString()}`; 38 export 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, ... } 34 53 } 35 54 36 // Broker-pubkey ophalen + cachen (bij fout NIET permanent cachen). 37 let _pubkeyPromise = null; 38 function getPublicKey() { 39 if (!_pubkeyPromise) { 40 _pubkeyPromise = (async () => { 41 const r = await fetch(`${BROKER_URL}/pubkey`); 42 if (!r.ok) throw new Error(`broker /pubkey faalde: ${r.status}`); 43 return importSPKI(await r.text(), ALG); 44 })().catch((e) => { 45 _pubkeyPromise = null; 46 throw e; 47 }); 48 } 49 return _pubkeyPromise; 55 // Returns { sub, email, email_verified, name, picture }. 56 export 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(); 50 60 } 51 52 // Verifieer het identity-token van de broker. Returnt de payload53 // { typ:'identity', sub, email, name, picture, jti, exp, ... }.54 export async function verifyIdentityToken(token) {55 const key = await getPublicKey();56 const { payload } = await jwtVerify(token, key, {57 issuer: ISSUER,58 algorithms: [ALG],59 audience: SITE_ORIGIN, // token moet voor ÓNZE site bedoeld zijn60 });61 if (payload.typ !== 'identity') throw new Error('verkeerd tokentype');62 return payload;63 }64 65 // Kleine in-memory jti-cache tegen replay binnen de (korte) geldigheidsduur.66 // Returnt false als de jti al gebruikt is.67 const _usedJti = new Map(); // jti -> exp (epoch seconds)68 export function consumeJti(jti, expEpoch) {69 if (!jti) return true; // geen jti = niets te dedupen70 const now = Math.floor(Date.now() / 1000);71 for (const [k, e] of _usedJti) if (e < now) _usedJti.delete(k);72 if (_usedJti.has(jti)) return false;73 _usedJti.set(jti, expEpoch || now + 600);74 return true;75 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)