/*********************************************************************************************************************************************************************** * File Name: google_oidc.util.js * Type of Program: Utility * Description: Manual Google OIDC primitives — no Passport. * Provides state/nonce generation, PKCE (S256), authorization URL construction, * authorization code exchange, and ID token verification via google-auth-library. * * OIDC flow summary: * 1. googleRedirect → generateState + generateNonce + generatePKCE → buildAuthUrl → redirect * 2. googleCallback → verify state cookie → exchangeCode → verifyIdToken → create/find user * * Security properties: * - state : anti-CSRF; verified against signed httpOnly cookie * - nonce : anti-replay; embedded in ID token by Google and checked here * - PKCE S256 : prevents auth code interception even if code leaks * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 21, 2026 ***********************************************************************************************************************************************************************/ 'use strict'; const crypto = require('crypto'); const { OAuth2Client } = require('google-auth-library'); const axios = require('axios'); const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'; // Lazily initialised so the module can be required before env is loaded. let _client; const getClient = () => { if (!_client) _client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); return _client; }; // ── Generators ───────────────────────────────────────────────────────────────── const generateState = () => crypto.randomBytes(32).toString('hex'); const generateNonce = () => crypto.randomBytes(32).toString('hex'); const generatePKCE = () => { const codeVerifier = crypto.randomBytes(32).toString('base64url'); const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); return { codeVerifier, codeChallenge }; }; // ── Auth URL ─────────────────────────────────────────────────────────────────── const buildAuthUrl = (state, nonce, codeChallenge) => { const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID, redirect_uri: process.env.GOOGLE_CALLBACK_URL, response_type: 'code', scope: 'openid email profile', state, nonce, code_challenge: codeChallenge, code_challenge_method: 'S256', access_type: 'offline', prompt: 'select_account', }); return `${GOOGLE_AUTH_URL}?${params.toString()}`; }; // ── Code exchange ────────────────────────────────────────────────────────────── const exchangeCode = async (code, codeVerifier) => { const { data } = await axios.post( GOOGLE_TOKEN_URL, new URLSearchParams({ code, client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET, redirect_uri: process.env.GOOGLE_CALLBACK_URL, grant_type: 'authorization_code', code_verifier: codeVerifier, }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, ); return data; // { access_token, id_token, expires_in, token_type, ... } }; // ── ID token verification ────────────────────────────────────────────────────── const verifyIdToken = async (idToken, expectedNonce) => { const ticket = await getClient().verifyIdToken({ idToken, audience: process.env.GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); // Verify nonce to prevent token replay attacks. if (payload.nonce !== expectedNonce) throw new Error('Nonce mismatch'); return payload; // { sub, email, given_name, family_name, name, picture, ... } }; module.exports = { generateState, generateNonce, generatePKCE, buildAuthUrl, exchangeCode, verifyIdToken };