import { createHmac, randomBytes, timingSafeEqual } from 'crypto'; const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'; const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'; export function isGoogleOAuthConfigured(): boolean { return Boolean(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); } export function getSiteOrigin(req?: Request): string { if (req) { try { const url = new URL(req.url); // In production behind nginx, prefer https even if internal request is http if (process.env.NODE_ENV === 'production' && url.protocol === 'http:') { url.protocol = 'https:'; } return url.origin; } catch {} } return process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'; } export function getGoogleRedirectUri(req?: Request): string { return `${getSiteOrigin(req)}/api/auth/callback/google`; } function getOAuthStateSecret(): string { return process.env.CUSTOMER_SESSION_SECRET ?? 'shahi-kitchen-customer-dev-secret'; } export function createOAuthState(): string { const nonce = randomBytes(16).toString('hex'); const signature = createHmac('sha256', getOAuthStateSecret()).update(nonce).digest('hex'); return `${nonce}.${signature}`; } export function verifyOAuthState(state: string | null | undefined): boolean { if (!state) return false; const [nonce, signature] = state.split('.'); if (!nonce || !signature) return false; const expected = createHmac('sha256', getOAuthStateSecret()).update(nonce).digest('hex'); const sigBuffer = Buffer.from(signature); const expectedBuffer = Buffer.from(expected); if (sigBuffer.length !== expectedBuffer.length) return false; return timingSafeEqual(sigBuffer, expectedBuffer); } export function buildGoogleAuthUrl(state: string, req?: Request): string { const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID!, redirect_uri: getGoogleRedirectUri(req), response_type: 'code', scope: 'openid email profile', access_type: 'online', prompt: 'select_account', state, }); return `${GOOGLE_AUTH_URL}?${params.toString()}`; } interface GoogleTokenResponse { access_token?: string; error?: string; } interface GoogleUserInfo { email?: string; email_verified?: boolean; name?: string; } export async function fetchGoogleUserFromCode( code: string, req?: Request, ): Promise<{ email: string; name?: string } | null> { const tokenResponse = await fetch(GOOGLE_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, client_id: process.env.GOOGLE_CLIENT_ID!, client_secret: process.env.GOOGLE_CLIENT_SECRET!, redirect_uri: getGoogleRedirectUri(req), grant_type: 'authorization_code', }), }); const tokenData = (await tokenResponse.json()) as GoogleTokenResponse; if (!tokenResponse.ok || !tokenData.access_token) return null; const userResponse = await fetch(GOOGLE_USERINFO_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` }, }); const userData = (await userResponse.json()) as GoogleUserInfo; if (!userResponse.ok || !userData.email || userData.email_verified === false) return null; return { email: userData.email, name: userData.name, }; }