/** * OAuth 2.0 authentication for Google Search Console * Supports interactive browser flow with token persistence */ import { OAuth2Client } from 'google-auth-library'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import open from 'open'; import { createServer } from 'http'; const SCOPES = [ 'https://www.googleapis.com/auth/webmasters', 'https://www.googleapis.com/auth/webmasters.readonly', 'https://www.googleapis.com/auth/indexing' ]; const TOKEN_DIR = join(homedir(), '.gsc-mcp'); const TOKEN_PATH = join(TOKEN_DIR, 'oauth-token.json'); const REDIRECT_URI = 'http://localhost:3000/oauth2callback'; export interface OAuthConfig { clientId: string; clientSecret: string; redirectUri?: string; } /** * Load OAuth credentials from environment or file */ export function loadOAuthConfig(): OAuthConfig | null { // Try environment variables first if (process.env.GSC_OAUTH_CLIENT_ID && process.env.GSC_OAUTH_CLIENT_SECRET) { return { clientId: process.env.GSC_OAUTH_CLIENT_ID, clientSecret: process.env.GSC_OAUTH_CLIENT_SECRET, redirectUri: process.env.GSC_OAUTH_REDIRECT_URI || REDIRECT_URI }; } // Try loading from file if (process.env.GSC_OAUTH_CLIENT_FILE) { try { const content = readFileSync(process.env.GSC_OAUTH_CLIENT_FILE, 'utf-8'); const data = JSON.parse(content); // Support both installed app and web app formats const credentials = data.installed || data.web; if (!credentials) { throw new Error('Invalid OAuth client file format'); } return { clientId: credentials.client_id, clientSecret: credentials.client_secret, redirectUri: credentials.redirect_uris?.[0] || REDIRECT_URI }; } catch (error: any) { throw new Error(`Failed to load OAuth config from file: ${error.message}`); } } return null; } /** * Load stored OAuth tokens from disk */ function loadStoredTokens(): any | null { if (!existsSync(TOKEN_PATH)) { return null; } try { const content = readFileSync(TOKEN_PATH, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Save OAuth tokens to disk */ function saveTokens(tokens: any): void { if (!existsSync(TOKEN_DIR)) { mkdirSync(TOKEN_DIR, { recursive: true }); } writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2)); } /** * Perform OAuth flow with browser-based consent */ async function performOAuthFlow(oauth2Client: OAuth2Client): Promise { const authUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES, prompt: 'consent' // Force consent to get refresh token }); console.error('Opening browser for Google authentication...'); console.error('If browser does not open, visit this URL:'); console.error(authUrl); // Create temporary HTTP server to capture callback const server = createServer(); const port = 3000; const tokenPromise = new Promise((resolve, reject) => { server.on('request', async (req, res) => { try { const url = new URL(req.url || '', `http://localhost:${port}`); if (url.pathname === '/oauth2callback') { const code = url.searchParams.get('code'); if (!code) { res.writeHead(400); res.end('Missing authorization code'); reject(new Error('Missing authorization code')); return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`

Authentication successful!

You can close this window and return to the terminal.

`); // Exchange code for tokens const { tokens } = await oauth2Client.getToken(code); resolve(tokens); server.close(); } } catch (error) { reject(error); server.close(); } }); server.on('error', (error) => { reject(error); }); }); server.listen(port); // Open browser await open(authUrl); // Wait for tokens const tokens = await tokenPromise; oauth2Client.setCredentials(tokens); saveTokens(tokens); } /** * Create authenticated OAuth2 client */ export async function getOAuthClient(): Promise { const config = loadOAuthConfig(); if (!config) { throw new Error( 'OAuth credentials not found. Set GSC_OAUTH_CLIENT_ID/SECRET or GSC_OAUTH_CLIENT_FILE' ); } const oauth2Client = new OAuth2Client( config.clientId, config.clientSecret, config.redirectUri ); // Try to load stored tokens const storedTokens = loadStoredTokens(); if (storedTokens) { oauth2Client.setCredentials(storedTokens); // Set up automatic token refresh oauth2Client.on('tokens', (tokens) => { if (tokens.refresh_token) { saveTokens({ ...storedTokens, ...tokens }); } else { saveTokens({ ...storedTokens, access_token: tokens.access_token }); } }); // Verify tokens are still valid try { await oauth2Client.getAccessToken(); return oauth2Client; } catch (error) { console.error('Stored tokens invalid, re-authenticating...'); } } // Perform interactive OAuth flow await performOAuthFlow(oauth2Client); // Set up automatic token refresh oauth2Client.on('tokens', (tokens) => { const current = loadStoredTokens() || {}; saveTokens({ ...current, ...tokens }); }); return oauth2Client; }