UNPKG

oauth-token-automation

Version:

Zero-setup OAuth authorization code flow automation with browser automation

391 lines (326 loc) 12.3 kB
const fs = require('fs'); const path = require('path'); const { chromium } = require('playwright'); class OAuthAutomation { constructor(config) { this.config = config; this.browser = null; this.authCode = null; } log(message, data = null) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${message}`); if (data) { console.log(JSON.stringify(data, null, 2)); } } extractCodeFromUrl(url) { this.log('✅ Extracting authorization code from URL'); try { const urlObj = new URL(url); const code = urlObj.searchParams.get('code'); const state = urlObj.searchParams.get('state'); const error = urlObj.searchParams.get('error'); if (error) { const errorDescription = urlObj.searchParams.get('error_description'); this.log('❌ Authorization error in URL', { error, errorDescription }); throw new Error(`Authorization error: ${error} - ${errorDescription || 'No description provided'}`); } if (code) { this.log('✅ Authorization code extracted successfully', { code, state }); return { code, state }; } else { this.log('❌ No authorization code found in URL'); throw new Error('No authorization code found in callback URL'); } } catch (error) { this.log('❌ Failed to extract code from URL', { error: error.message }); throw error; } } buildAuthUrl() { const params = new URLSearchParams({ client_id: this.config.client_id, response_type: this.config.response_type, redirect_uri: this.config.redirect_uri, scope: this.config.scope, state: this.config.state }); const authUrl = `${this.config.authentication_url}?${params.toString()}`; this.log('🔗 Built authorization URL'); return authUrl; } async automateLogin() { this.log('🌐 Starting browser automation...'); this.browser = await chromium.launch({ headless: this.config.headless, timeout: this.config.timeout }); const context = await this.browser.newContext(); const page = await context.newPage(); try { const authUrl = this.buildAuthUrl(); this.log('📂 Navigating to authorization URL...'); await page.goto(authUrl); // Fill username with configurable selectors this.log('👤 Filling username...'); const usernameSelectors = this.config.selectors?.username || [ 'input[name="username"]', 'input[id="user_username"]', 'input[type="email"]', '#username', '#email', 'input[name="user"]', 'input[id*="username"]', 'input[id*="email"]' ]; let usernameField = null; for (const selector of usernameSelectors) { try { await page.waitForSelector(selector, { timeout: 2000 }); usernameField = page.locator(selector).first(); this.log('✅ Found username field with selector:', { selector }); break; } catch (e) { this.log('🔍 Username selector not found, trying next:', { selector }); } } if (!usernameField) { throw new Error('Username field not found with any selector'); } await usernameField.fill(this.config.username); this.log('✅ Username filled'); // Fill password with configurable selectors this.log('🔐 Filling password...'); const passwordSelectors = this.config.selectors?.password || [ 'input[name="password"]', 'input[type="password"]', 'input[id="user_password"]', '#password', 'input[id*="password"]', 'input[id*="pwd"]' ]; let passwordField = null; for (const selector of passwordSelectors) { try { await page.waitForSelector(selector, { timeout: 2000 }); passwordField = page.locator(selector).first(); this.log('✅ Found password field with selector:', { selector }); break; } catch (e) { this.log('🔍 Password selector not found, trying next:', { selector }); } } if (!passwordField) { throw new Error('Password field not found with any selector'); } await passwordField.fill(this.config.password); this.log('✅ Password filled'); // Submit form with configurable selectors this.log('🚀 Submitting login form...'); const submitSelectors = this.config.selectors?.submit || [ 'button[type="submit"]', 'button[id="submit"]', 'input[type="submit"]', 'button:has-text("Sign in")', 'button:has-text("Login")', 'button:has-text("Log in")', '.btn-login', '.login-btn', '#submit' ]; let submitted = false; for (const selector of submitSelectors) { try { const submitButton = page.locator(selector).first(); if (await submitButton.isVisible()) { await submitButton.click(); this.log('✅ Clicked submit button with selector:', { selector }); submitted = true; break; } } catch (e) { this.log('🔍 Submit selector not found or not clickable, trying next:', { selector }); } } if (!submitted) { this.log('⌨️ No submit button found, trying Enter key...'); await page.keyboard.press('Enter'); } // Intercept the redirect to callback URL without actually navigating this.log('⏳ Waiting for authorization redirect...'); const callbackUrl = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for OAuth callback redirect')); }, this.config.timeout); // Listen for navigation attempts to the callback URL page.on('framenavigated', (frame) => { if (frame === page.mainFrame()) { const url = frame.url(); this.log('🔍 Page navigated to:', { url }); // Check if this is the callback URL if (url.startsWith(this.config.redirect_uri)) { clearTimeout(timeout); this.log('✅ Callback URL intercepted', { url }); resolve(url); } } }); // Also listen for requests to the callback URL page.on('request', (request) => { const requestUrl = request.url(); if (requestUrl.startsWith(this.config.redirect_uri)) { clearTimeout(timeout); this.log('✅ Callback request intercepted'); resolve(requestUrl); } }); }); // Extract authorization code from URL const { code, state } = this.extractCodeFromUrl(callbackUrl); this.authCode = code; // Verify state parameter if provided if (this.config.state && state !== this.config.state) { this.log('❌ State parameter mismatch', { expected: this.config.state, received: state }); throw new Error('State parameter mismatch - possible CSRF attack'); } this.log('✅ Authorization code captured successfully'); } catch (error) { this.log('❌ Browser automation failed', { error: error.message }); throw error; } finally { if (this.browser) { await this.browser.close(); this.log('🔒 Browser closed'); } } } async exchangeCodeForToken() { if (!this.authCode) { throw new Error('No authorization code available'); } this.log('🔄 Exchanging authorization code for token...'); const tokenData = { grant_type: 'authorization_code', code: this.authCode, redirect_uri: this.config.redirect_uri, client_id: this.config.client_id, client_secret: this.config.client_secret }; this.log('📤 Sending token request'); try { const response = await fetch(this.config.token_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: new URLSearchParams(tokenData) }); const responseText = await response.text(); this.log('📥 Token response received', { status: response.status, statusText: response.statusText }); if (!response.ok) { this.log('❌ Token exchange failed', { response: responseText }); throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`); } const tokenResponse = JSON.parse(responseText); this.log('✅ Token exchange successful'); return tokenResponse; } catch (error) { this.log('❌ Token exchange error', { error: error.message }); throw error; } } async copyToClipboard(text) { try { // Try to import clipboardy - handle both CommonJS and ES modules let clipboardy; try { clipboardy = require('clipboardy'); } catch (requireError) { // If require fails, try dynamic import for ES modules const { default: clipboardyModule } = await import('clipboardy'); clipboardy = clipboardyModule; } await clipboardy.write(text); this.log('📋 Access token copied to clipboard'); return true; } catch (error) { this.log('⚠️ Failed to copy to clipboard:', { error: error.message }); this.log('💡 You can manually copy the token from the output above'); return false; } } async cleanup() { this.log('🧹 Cleaning up resources...'); if (this.browser) { await this.browser.close(); this.log('✅ Browser closed'); } } async run() { try { this.log('🎯 Starting OAuth automation process...'); // Automate login and get authorization code await this.automateLogin(); // Exchange code for token const tokens = await this.exchangeCodeForToken(); this.log('🎉 OAuth automation completed successfully!'); this.log('🔑 Tokens received'); // Log the actual access token value if (tokens.access_token) { this.log('🎯 ACCESS TOKEN VALUE:', { access_token: tokens.access_token }); // Copy access token to clipboard await this.copyToClipboard(tokens.access_token); } return tokens; } catch (error) { this.log('💥 OAuth automation failed', { error: error.message }); throw error; } finally { await this.cleanup(); } } } // Main execution async function main() { const args = process.argv.slice(2); const envArg = args.find(arg => arg.startsWith('--env=')); const environment = envArg ? envArg.split('=')[1] : 'dev'; console.log(`🌍 Loading configuration for environment: ${environment}`); const configPath = path.join(__dirname, `config.${environment}.json`); if (!fs.existsSync(configPath)) { console.error(`❌ Configuration file not found: ${configPath}`); console.error('Available configurations:'); const configFiles = fs.readdirSync(__dirname) .filter(file => file.startsWith('config.') && file.endsWith('.json')) .map(file => file.replace('config.', '').replace('.json', '')); console.error(` ${configFiles.join(', ')}`); process.exit(1); } try { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); console.log('✅ Configuration loaded successfully'); const automation = new OAuthAutomation(config); const tokens = await automation.run(); } catch (error) { console.error('💥 Application failed:', error.message); process.exit(1); } } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n🛑 Received SIGINT, shutting down gracefully...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('\n🛑 Received SIGTERM, shutting down gracefully...'); process.exit(0); }); if (require.main === module) { main(); } module.exports = { OAuthAutomation };