Skip to main content
Glama
auth-service.ts7.01 kB
import { google } from 'googleapis'; import { OAuth2Client, GoogleAuth } from 'google-auth-library'; import { promises as fs } from 'fs'; import path from 'path'; import { AuthenticationError, RetryableError } from '../types/errors.js'; import { GoogleConfig } from '../types/index.js'; const TOKEN_PATH = path.join(process.cwd(), '/tmp/.tokens.json'); /** * Authentication service for Google APIs * Handles OAuth 2.0 flow and token management */ export class AuthService { private authClient: OAuth2Client | GoogleAuth | null = null; private config: GoogleConfig; constructor(config?: GoogleConfig) { this.config = config || this.loadConfigFromEnv(); this.initializeAuth(); } private loadConfigFromEnv(): GoogleConfig { const clientId = process.env.GOOGLE_CLIENT_ID; const clientSecret = process.env.GOOGLE_CLIENT_SECRET; const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/oauth/callback'; const serviceAccountPath = process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH; if (serviceAccountPath) { return { type: 'service_account', credentials: serviceAccountPath, }; } if (!clientId || !clientSecret) { throw new AuthenticationError( 'Google OAuth credentials not found. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.' ); } return { type: 'oauth', clientId, clientSecret, redirectUri, }; } private initializeAuth(): void { if (this.config.type === 'service_account') { this.initializeServiceAccount(); } else { this.initializeOAuth2(); } } private initializeOAuth2(): void { if (!this.config.clientId || !this.config.clientSecret) { throw new AuthenticationError('OAuth credentials not provided'); } this.authClient = new OAuth2Client( this.config.clientId, this.config.clientSecret, this.config.redirectUri ); } private initializeServiceAccount(): void { if (!this.config.credentials) { throw new AuthenticationError('Service account credentials path not provided'); } try { this.authClient = new GoogleAuth({ keyFile: this.config.credentials, scopes: [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive.readonly', ], }); } catch (error) { throw new AuthenticationError(`Failed to initialize service account: ${error}`); } } /** * Get authenticated client */ async getAuthenticatedClient(): Promise<OAuth2Client | GoogleAuth> { if (!this.authClient) { throw new AuthenticationError('Auth client not initialized'); } // For service account, return immediately if (this.config.type === 'service_account') { return this.authClient; } // For OAuth flow, check if we have valid tokens const tokens = await this.getStoredTokens(); if (tokens && this.authClient instanceof OAuth2Client) { this.authClient.setCredentials(tokens); // Check if token is expired and refresh if needed if (this.isTokenExpired(tokens)) { await this.refreshToken(); } return this.authClient; } throw new AuthenticationError('No valid authentication tokens found. Please authenticate first.'); } /** * Generate OAuth2 authorization URL */ getAuthUrl(): string { if (!this.authClient || this.config.type === 'service_account' || !(this.authClient instanceof OAuth2Client)) { throw new AuthenticationError('OAuth2 client not available for authorization URL generation'); } const scopes = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive.readonly', ]; return this.authClient.generateAuthUrl({ access_type: 'offline', scope: scopes, prompt: 'consent', // Force consent to get refresh token }); } /** * Exchange authorization code for tokens */ async exchangeCodeForTokens(code: string): Promise<void> { if (!this.authClient || !(this.authClient instanceof OAuth2Client)) { throw new AuthenticationError('OAuth2 client not initialized'); } try { const { tokens } = await this.authClient.getToken(code); this.authClient.setCredentials(tokens); await this.storeTokens(tokens); } catch (error) { throw new AuthenticationError(`Failed to exchange code for tokens: ${error}`); } } /** * Refresh access token using refresh token */ private async refreshToken(): Promise<void> { if (!this.authClient || !(this.authClient instanceof OAuth2Client)) { throw new AuthenticationError('OAuth2 client not initialized'); } try { const { credentials } = await this.authClient.refreshAccessToken(); this.authClient.setCredentials(credentials); await this.storeTokens(credentials); } catch (error) { throw new AuthenticationError(`Failed to refresh token: ${error}`); } } /** * Check if token is expired */ private isTokenExpired(tokens: any): boolean { if (!tokens.expiry_date) { return true; } return Date.now() >= tokens.expiry_date; } /** * Store tokens securely (implement based on your storage preference) */ private async storeTokens(tokens: any): Promise<void> { await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2)); console.log('Tokens stored to', TOKEN_PATH); } /** * Retrieve stored tokens */ private async getStoredTokens(): Promise<any> { try { const content = await fs.readFile(TOKEN_PATH, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Check if user is authenticated */ async isAuthenticated(): Promise<boolean> { try { await this.getAuthenticatedClient(); return true; } catch { return false; } } /** * Revoke authentication tokens */ async revokeTokens(): Promise<void> { if (!this.authClient || !(this.authClient instanceof OAuth2Client)) { return; } try { await this.authClient.revokeCredentials(); // Clear stored tokens await this.clearStoredTokens(); } catch (error) { console.error('Failed to revoke tokens:', error); } } /** * Clear stored tokens */ private async clearStoredTokens(): Promise<void> { try { await fs.unlink(TOKEN_PATH); console.log('Tokens cleared'); } catch (error) { // Ignore errors if file doesn't exist } } /** * Get Google Sheets API client */ async getSheetsClient() { const auth = await this.getAuthenticatedClient(); return google.sheets({ version: 'v4', auth }); } /** * Get Google Drive API client */ async getDriveClient() { const auth = await this.getAuthenticatedClient(); return google.drive({ version: 'v3', auth }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ainetwork-ai/google-sheet-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server