Changeset 32cc601 in Klonkt for src/config
- Timestamp:
- 06/14/2026 06:55:51 AM (3 months ago)
- Branches:
- main
- Children:
- 9e27d64
- Parents:
- ae924a2
- File:
-
- 1 edited
-
src/config/google.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/google.js
rae924a2 r32cc601 1 // Google OAuth2 (per-instance). Raw via de ingebouwde fetch — geen passport-dep. 2 // Config via env (per instance, in .env): 3 // GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET 4 // GOOGLE_REDIRECT_URI = https://<dit-domein>/auth/google/callback 5 // ADMIN_EMAIL = de Google-mail die owner/admin (god) is op deze instance 6 // Niet geconfigureerd? Dan booten we gewoon door; /auth/google meldt netjes 7 // "nog niet geconfigureerd" i.p.v. te crashen. 1 // Google-login via de centrale Klonkt-broker (license.klonkt.com). 2 // 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. 7 // 8 // 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. 8 13 9 const CLIENT_ID = process.env.GOOGLE_CLIENT_ID || ''; 10 const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || ''; 11 const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || ''; 14 import { importSPKI, jwtVerify } from 'jose'; 12 15 13 const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; 14 const TOKEN_URL = 'https://oauth2.googleapis.com/token'; 15 const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo'; 16 const ALG = 'EdDSA'; 17 const ISSUER = 'klonkt-license'; 16 18 17 export function googleConfigured() { 18 return !!(CLIENT_ID && CLIENT_SECRET && REDIRECT_URI); 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); 19 24 } 20 25 21 export function authorizeUrl(state) { 22 const p = new URLSearchParams({ 23 client_id: CLIENT_ID, 24 redirect_uri: REDIRECT_URI, 25 response_type: 'code', 26 scope: 'openid email profile', 27 state, 28 access_type: 'online', 29 prompt: 'select_account', 30 }); 31 return `${AUTH_URL}?${p.toString()}`; 26 // Waar de broker naartoe terugstuurt (moet in de broker-allowlist staan). 27 export function callbackUrl() { 28 return `${SITE_ORIGIN}/auth/google/callback`; 32 29 } 33 30 34 export async function exchangeCode(code) { 35 const body = new URLSearchParams({ 36 code, 37 client_id: CLIENT_ID, 38 client_secret: CLIENT_SECRET, 39 redirect_uri: REDIRECT_URI, 40 grant_type: 'authorization_code', 41 }); 42 const r = await fetch(TOKEN_URL, { 43 method: 'POST', 44 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 45 body, 46 }); 47 if (!r.ok) throw new Error(`Google token-exchange faalde: ${r.status} ${await r.text().catch(() => '')}`); 48 return r.json(); // { access_token, id_token, ... } 31 export function brokerStartUrl(istate) { 32 const p = new URLSearchParams({ return: callbackUrl(), istate }); 33 return `${BROKER_URL}/auth/google/start?${p.toString()}`; 49 34 } 50 35 51 // Returns { sub, email, email_verified, name, picture }. 52 export async function fetchUserinfo(accessToken) { 53 const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } }); 54 if (!r.ok) throw new Error(`Google userinfo faalde: ${r.status}`); 55 return r.json(); 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; 56 50 } 51 52 // Verifieer het identity-token van de broker. Returnt de payload 53 // { 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 zijn 60 }); 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 dedupen 70 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)