Files
shahikitchen-prod/infrastructure/auth/google-oauth.ts
T

95 lines
3.0 KiB
TypeScript

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(): string {
return process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';
}
export function getGoogleRedirectUri(): string {
return `${getSiteOrigin()}/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): string {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: getGoogleRedirectUri(),
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,
): 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(),
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,
};
}