import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import * as fs from 'fs';
import * as path from 'path';
import {
getConfig,
getOAuthScopes,
AuthError,
InvalidCredentialsError,
TokenExpiredError,
isTokenExpired,
getLogger,
type AuthCredentials,
} from '@company-mcp/core';
const logger = getLogger();
let authClient: OAuth2Client | null = null;
// Load OAuth credentials from file
function loadOAuthCredentials(): { client_id: string; client_secret: string; redirect_uri: string } {
const config = getConfig();
// First try environment variables
if (config.oauth_client_id && config.oauth_client_secret) {
return {
client_id: config.oauth_client_id,
client_secret: config.oauth_client_secret,
redirect_uri: config.oauth_redirect_uri,
};
}
// Then try credentials file
const credentialsPath = path.resolve('.secrets/credentials.json');
if (!fs.existsSync(credentialsPath)) {
throw new InvalidCredentialsError(
`OAuth credentials not found. Either set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET env vars, or create ${credentialsPath}`
);
}
const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'));
// Handle different credential file formats
const creds = credentials.installed || credentials.web || credentials;
return {
client_id: creds.client_id,
client_secret: creds.client_secret,
redirect_uri: creds.redirect_uris?.[0] || config.oauth_redirect_uri,
};
}
// Load saved token
function loadToken(): AuthCredentials | null {
const config = getConfig();
const tokenPath = path.resolve(config.oauth_token_path);
if (!fs.existsSync(tokenPath)) {
return null;
}
try {
const token = JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
return token as AuthCredentials;
} catch {
logger.warn('google-auth', 'Failed to load token file');
return null;
}
}
// Save token
function saveToken(token: AuthCredentials): void {
const config = getConfig();
const tokenPath = path.resolve(config.oauth_token_path);
const tokenDir = path.dirname(tokenPath);
if (!fs.existsSync(tokenDir)) {
fs.mkdirSync(tokenDir, { recursive: true });
}
fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2));
logger.info('google-auth', 'Token saved', { path: tokenPath });
}
// Create OAuth2 client
export function createOAuthClient(): OAuth2Client {
const creds = loadOAuthCredentials();
const client = new google.auth.OAuth2(
creds.client_id,
creds.client_secret,
creds.redirect_uri
);
// Load existing token
const token = loadToken();
if (token) {
client.setCredentials({
access_token: token.access_token,
refresh_token: token.refresh_token,
expiry_date: token.expiry_date,
});
}
// Auto-refresh on token expiry
client.on('tokens', (tokens) => {
logger.info('google-auth', 'Token refreshed');
// Merge with existing token (keep refresh_token if not returned)
const existingToken = loadToken();
const newToken: AuthCredentials = {
access_token: tokens.access_token!,
refresh_token: tokens.refresh_token || existingToken?.refresh_token,
expiry_date: tokens.expiry_date ?? undefined,
token_type: 'Bearer',
scope: tokens.scope ?? undefined,
};
saveToken(newToken);
});
return client;
}
// Generate OAuth authorization URL
export function getAuthUrl(redirectUri?: string): string {
const client = createOAuthClient();
const scopes = getOAuthScopes();
const options: {
access_type: 'offline';
scope: string[];
prompt: 'consent';
redirect_uri?: string;
} = {
access_type: 'offline',
scope: scopes,
prompt: 'consent', // Force consent to get refresh_token
};
if (redirectUri) {
options.redirect_uri = redirectUri;
}
const url = client.generateAuthUrl(options);
logger.info('google-auth', 'Generated auth URL', { scopes });
return url;
}
// Exchange authorization code for tokens
export async function exchangeCode(code: string, redirectUri?: string): Promise<AuthCredentials> {
const client = createOAuthClient();
try {
const { tokens } = await client.getToken({
code,
redirect_uri: redirectUri,
});
const authCredentials: AuthCredentials = {
access_token: tokens.access_token!,
refresh_token: tokens.refresh_token!,
expiry_date: tokens.expiry_date!,
token_type: 'Bearer',
scope: tokens.scope,
};
saveToken(authCredentials);
logger.info('google-auth', 'Token obtained successfully');
return authCredentials;
} catch (error) {
logger.error('google-auth', 'Failed to exchange code', error as Error);
throw new AuthError(
'Failed to exchange authorization code',
'CODE_EXCHANGE_FAILED',
error
);
}
}
// Create Service Account client
export function createServiceAccountClient(): OAuth2Client {
const config = getConfig();
const saPath = path.resolve(config.service_account_path);
if (!fs.existsSync(saPath)) {
throw new InvalidCredentialsError(
`Service account key not found at ${saPath}`
);
}
const serviceAccount = JSON.parse(fs.readFileSync(saPath, 'utf-8'));
const auth = new google.auth.GoogleAuth({
credentials: serviceAccount,
scopes: getOAuthScopes(),
});
logger.info('google-auth', 'Service account client created', {
email: serviceAccount.client_email,
});
return auth as unknown as OAuth2Client;
}
// Get authenticated client (auto-select based on config)
export function getAuthClient(): OAuth2Client {
if (authClient) {
return authClient;
}
const config = getConfig();
if (config.auth_mode === 'service_account') {
authClient = createServiceAccountClient();
} else {
authClient = createOAuthClient();
// Check if we have a valid token
const token = loadToken();
if (!token) {
throw new AuthError(
'No OAuth token found. Run `pnpm auth:oauth` to authenticate.',
'NO_TOKEN'
);
}
if (isTokenExpired(token.expiry_date) && !token.refresh_token) {
throw new TokenExpiredError(
'OAuth token expired and no refresh token available. Run `pnpm auth:oauth` to re-authenticate.'
);
}
}
logger.info('google-auth', `Using ${config.auth_mode} authentication`);
return authClient;
}
// Reset auth client (for testing)
export function resetAuthClient(): void {
authClient = null;
}
// Get Google API client with auth
export function getGoogleAPIs() {
const auth = getAuthClient();
return {
gmail: google.gmail({ version: 'v1', auth }),
drive: google.drive({ version: 'v3', auth }),
sheets: google.sheets({ version: 'v4', auth }),
docs: google.docs({ version: 'v1', auth }),
calendar: google.calendar({ version: 'v3', auth }),
};
}