oauth-token-automation
Version:
Zero-setup OAuth authorization code flow automation with browser automation
391 lines (326 loc) • 12.3 kB
JavaScript
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 };