Changeset 32cc601 in Klonkt for src/routes
- Timestamp:
- 06/14/2026 06:55:51 AM (3 months ago)
- Branches:
- main
- Children:
- 9e27d64
- Parents:
- ae924a2
- File:
-
- 1 edited
-
src/routes/auth.js (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/auth.js
rae924a2 r32cc601 5 5 import { renderPage } from '../middleware/render.js'; 6 6 import { safeNext } from '../middleware/auth.js'; 7 import { googleConfigured, authorizeUrl, exchangeCode, fetchUserinfo} from '../config/google.js';7 import { brokerConfigured, brokerStartUrl, verifyIdentityToken, consumeJti } from '../config/google.js'; 8 8 9 9 const router = express.Router(); 10 10 const ADMIN_EMAIL = (process.env.ADMIN_EMAIL || '').trim().toLowerCase(); 11 11 12 // ==================== LOGIN (alleen Google) ==================== 13 // Het oude username/wachtwoord-systeem is verwijderd; inloggen gaat via Google. 12 // ==================== LOGIN (Google via Klonkt-broker) ==================== 13 // Het oude username/wachtwoord-systeem is verwijderd; inloggen gaat via Google, 14 // gerouteerd door de centrale broker (geen Google-creds op deze instance). 14 15 router.get('/login', (req, res) => { 15 16 const next = safeNext(req.query.next) || ''; … … 18 19 pageTitle: 'Inloggen', 19 20 bodyClass: 'on-special', 20 googleReady: googleConfigured(),21 googleReady: brokerConfigured(), 21 22 error: req.query.error || null, 22 23 next, … … 24 25 }); 25 26 26 // Start de Google OAuth-flow.27 // Start de login: random state in de sessie (CSRF), dan door naar de broker. 27 28 router.get('/google', (req, res) => { 28 if (! googleConfigured()) {29 if (!brokerConfigured()) { 29 30 return res.redirect('/auth/login?error=' + encodeURIComponent('Google-login is op deze site nog niet geconfigureerd.')); 30 31 } 31 const next = safeNext(req.query.next) || ''; 32 const state = crypto.randomBytes(16).toString('hex'); 33 req.session.oauthState = state; 34 req.session.oauthNext = next; 35 res.redirect(authorizeUrl(state)); 32 const istate = crypto.randomBytes(16).toString('hex'); 33 req.session.loginState = istate; 34 req.session.loginNext = safeNext(req.query.next) || ''; 35 res.redirect(brokerStartUrl(istate)); 36 36 }); 37 37 … … 48 48 } 49 49 50 // Google callback: wissel code in, haal profiel, vind-of-maak user (op e-mail), 51 // zet de sessie. ADMIN_EMAIL bepaalt wie owner/admin (god) is. 50 // Callback van de broker: ?klonkt_id_token=&klonkt_state=(&klonkt_login_error=). 51 // We checken state (CSRF), verifiëren het token (sig + audience + replay), en 52 // vinden-of-maken de user op e-mail. ADMIN_EMAIL bepaalt wie owner/admin (god) is. 52 53 router.get('/google/callback', async (req, res) => { 53 54 const fail = (msg) => res.redirect('/auth/login?error=' + encodeURIComponent(msg)); 54 55 try { 55 const { code, state } = req.query; 56 if (!code || !state || state !== req.session.oauthState) { 57 return fail('Login afgebroken of ongeldige sessie.'); 56 const { klonkt_id_token: idToken, klonkt_state: state, klonkt_login_error: loginError } = req.query; 57 58 // State-check (CSRF) eerst — bind de callback aan de sessie die login startte. 59 const expected = req.session.loginState; 60 const next = safeNext(req.session.loginNext) || ''; 61 delete req.session.loginState; 62 delete req.session.loginNext; 63 if (!expected || state !== expected) return fail('Login afgebroken of ongeldige sessie.'); 64 65 if (loginError === 'unverified_email') return fail('Geen geverifieerd Google-e-mailadres.'); 66 if (!idToken) return fail('Geen logintoken ontvangen.'); 67 68 let payload; 69 try { 70 payload = await verifyIdentityToken(String(idToken)); 71 } catch (e) { 72 console.error('[auth/google/callback] tokenverificatie:', e.message); 73 return fail('Logintoken ongeldig of verlopen.'); 58 74 } 59 const next = safeNext(req.session.oauthNext) || ''; 60 delete req.session.oauthState; 61 delete req.session.oauthNext; 75 if (!consumeJti(payload.jti, payload.exp)) return fail('Logintoken al gebruikt.'); 62 76 63 const tok = await exchangeCode(String(code)); 64 const info = await fetchUserinfo(tok.access_token); 65 const email = (info.email || '').trim().toLowerCase(); 66 if (!email || info.email_verified === false) { 67 return fail('Geen geverifieerd Google-e-mailadres ontvangen.'); 68 } 77 const email = (payload.email || '').trim().toLowerCase(); 78 if (!email) return fail('Geen e-mailadres in logintoken.'); 69 79 70 80 const isAdmin = !!ADMIN_EMAIL && email === ADMIN_EMAIL; … … 73 83 if (!user) { 74 84 const userCount = db.prepare('SELECT COUNT(*) AS c FROM users').get().c; 75 const role = (isAdmin || userCount === 0) ? 'god' : 'member'; 85 // god = de ingestelde ADMIN_EMAIL. De "eerste user wordt god"-bootstrap geldt 86 // ALLEEN als er helemaal geen ADMIN_EMAIL is ingesteld — anders zou een vreemde 87 // die toevallig als eerste inlogt op een verse install eigenaar worden. 88 const role = isAdmin ? 'god' : (!ADMIN_EMAIL && userCount === 0 ? 'god' : 'member'); 76 89 const userId = uuid(); 77 const username = uniqueUsername( info.name || email.split('@')[0]);78 // password_hash is NOT NULL in het schema; we zetten een onbruikbare79 // sentinel (er is geen wachtwoord-login meer).90 const username = uniqueUsername(payload.name || email.split('@')[0]); 91 // password_hash is NOT NULL in het schema; er is geen wachtwoord-login meer, 92 // dus we zetten een onbruikbare sentinel. 80 93 db.prepare(` 81 94 INSERT INTO users (id, username, email, password_hash, role, avatar_url, theme, palette, google_sub) 82 95 VALUES (?, ?, ?, '!google-oauth', ?, ?, 'dark', 'sage', ?) 83 `).run(userId, username, info.email || email, role, info.picture || null, info.sub || null);96 `).run(userId, username, payload.email || email, role, payload.picture || null, payload.sub || null); 84 97 85 98 // Eerste/admin-user krijgt een persoonlijke site (zoals de oude flow), maar … … 95 108 user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); 96 109 } else { 110 // Identiteits-integriteit: als deze user al aan een ander Google-account 111 // gekoppeld is (google_sub mismatch), weiger — voorkomt overname als een 112 // geverifieerd e-mailadres ooit naar een andere Google-sub verhuist. 113 if (user.google_sub && payload.sub && user.google_sub !== payload.sub) { 114 return fail('Dit e-mailadres is al aan een ander Google-account gekoppeld.'); 115 } 97 116 // Bestaande user (gekoppeld op e-mail): koppel Google-id + avatar als die 98 117 // nog ontbreken, en promoveer naar god als dit ADMIN_EMAIL is. … … 103 122 updated_at = CURRENT_TIMESTAMP 104 123 WHERE id = ? 105 `).run( info.sub || null, info.picture || null, user.id);124 `).run(payload.sub || null, payload.picture || null, user.id); 106 125 if (isAdmin && user.role !== 'god') { 107 126 db.prepare("UPDATE users SET role = 'god' WHERE id = ?").run(user.id);
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)