#!/usr/bin/env npx tsx /** * Authentication handler for web automation * Supports generic form login and Microsoft SSO (MSAL) * * Usage: * npx tsx auth.ts --url "https://example.com/login" --type form * npx tsx auth.ts --url "https://example.com" --type msal * npx tsx auth.ts --url "https://example.com" --type auto */ import { getPage, launchBrowser } from './browse.js'; import parseArgs from 'minimist'; import type { Page, BrowserContext } from 'playwright-core'; import { createInterface } from 'readline'; // Types type AuthType = 'auto' | 'form' | 'msal'; interface AuthOptions { url: string; authType: AuthType; credentials?: { username: string; password: string; }; headless?: boolean; timeout?: number; } interface AuthResult { success: boolean; finalUrl: string; authType: AuthType; message: string; } // Get credentials from environment or options function getCredentials(options?: { username?: string; password?: string; }): { username: string; password: string } | null { const username = options?.username || process.env.CLOAKBROWSER_USERNAME || process.env.CAMOUFOX_USERNAME; const password = options?.password || process.env.CLOAKBROWSER_PASSWORD || process.env.CAMOUFOX_PASSWORD; if (!username || !password) { return null; } return { username, password }; } // Prompt user for input (for MFA or credentials) async function promptUser(question: string, hidden = false): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { if (hidden) { process.stdout.write(question); // Note: This is a simple implementation. For production, use a proper hidden input library } rl.question(question, (answer) => { rl.close(); resolve(answer); }); }); } // Detect authentication type from page async function detectAuthType(page: Page): Promise { const url = page.url(); // Check for Microsoft login if ( url.includes('login.microsoftonline.com') || url.includes('login.live.com') || url.includes('login.windows.net') ) { return 'msal'; } // Check for common form login patterns const hasLoginForm = await page.evaluate(() => { const passwordField = document.querySelector( 'input[type="password"], input[name*="password"], input[id*="password"]' ); const usernameField = document.querySelector( 'input[type="email"], input[type="text"][name*="user"], input[type="text"][name*="email"], input[id*="user"], input[id*="email"]' ); return !!(passwordField && usernameField); }); if (hasLoginForm) { return 'form'; } return 'auto'; } // Handle generic form login async function handleFormLogin( page: Page, credentials: { username: string; password: string }, timeout: number ): Promise { console.log('Attempting form login...'); // Find and fill username/email field const usernameSelectors = [ 'input[type="email"]', 'input[name*="user" i]', 'input[name*="email" i]', 'input[id*="user" i]', 'input[id*="email" i]', 'input[autocomplete="username"]', 'input[type="text"]:first-of-type', ]; let usernameField = null; for (const selector of usernameSelectors) { usernameField = await page.$(selector); if (usernameField) break; } if (!usernameField) { console.error('Could not find username/email field'); return false; } await usernameField.fill(credentials.username); console.log('Filled username field'); // Find and fill password field const passwordSelectors = [ 'input[type="password"]', 'input[name*="password" i]', 'input[id*="password" i]', 'input[autocomplete="current-password"]', ]; let passwordField = null; for (const selector of passwordSelectors) { passwordField = await page.$(selector); if (passwordField) break; } if (!passwordField) { console.error('Could not find password field'); return false; } await passwordField.fill(credentials.password); console.log('Filled password field'); // Check for "Remember me" checkbox and check it const rememberCheckbox = await page.$( 'input[type="checkbox"][name*="remember" i], input[type="checkbox"][id*="remember" i]' ); if (rememberCheckbox) { await rememberCheckbox.check(); console.log('Checked "Remember me" checkbox'); } // Find and click submit button const submitSelectors = [ 'button[type="submit"]', 'input[type="submit"]', 'button:has-text("Sign in")', 'button:has-text("Log in")', 'button:has-text("Login")', 'button:has-text("Submit")', '[role="button"]:has-text("Sign in")', ]; let submitButton = null; for (const selector of submitSelectors) { submitButton = await page.$(selector); if (submitButton) break; } if (!submitButton) { // Try pressing Enter as fallback await passwordField.press('Enter'); } else { await submitButton.click(); } console.log('Submitted login form'); // Wait for navigation or error try { await page.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }); return true; } catch { // Check if we're still on login page with error const errorMessages = await page.$$eval( '.error, .alert-danger, [role="alert"], .login-error', (els) => els.map((el) => el.textContent?.trim()).filter(Boolean) ); if (errorMessages.length > 0) { console.error('Login error:', errorMessages.join(', ')); return false; } return true; // Might have succeeded without navigation } } // Handle Microsoft SSO login async function handleMsalLogin( page: Page, credentials: { username: string; password: string }, timeout: number ): Promise { console.log('Attempting Microsoft SSO login...'); const currentUrl = page.url(); // If not already on Microsoft login, wait for redirect if (!currentUrl.includes('login.microsoftonline.com')) { try { await page.waitForURL('**/login.microsoftonline.com/**', { timeout: 10000 }); } catch { console.log('Not redirected to Microsoft login'); return false; } } // Wait for email input const emailInput = await page.waitForSelector( 'input[type="email"], input[name="loginfmt"]', { timeout } ); if (!emailInput) { console.error('Could not find email input on Microsoft login'); return false; } // Fill email and submit await emailInput.fill(credentials.username); console.log('Filled email field'); const nextButton = await page.$('input[type="submit"], button[type="submit"]'); if (nextButton) { await nextButton.click(); } else { await emailInput.press('Enter'); } // Wait for password page try { await page.waitForSelector( 'input[type="password"], input[name="passwd"]', { timeout } ); } catch { // Might be using passwordless auth or different flow console.log('Password field not found - might be using different auth flow'); return false; } // Fill password const passwordInput = await page.$('input[type="password"], input[name="passwd"]'); if (!passwordInput) { console.error('Could not find password input'); return false; } await passwordInput.fill(credentials.password); console.log('Filled password field'); // Submit const signInButton = await page.$('input[type="submit"], button[type="submit"]'); if (signInButton) { await signInButton.click(); } else { await passwordInput.press('Enter'); } // Handle "Stay signed in?" prompt try { const staySignedInButton = await page.waitForSelector( 'input[value="Yes"], button:has-text("Yes")', { timeout: 5000 } ); if (staySignedInButton) { await staySignedInButton.click(); console.log('Clicked "Stay signed in" button'); } } catch { // Prompt might not appear } // Check for Conditional Access Policy error const caError = await page.$('text=Conditional Access policy'); if (caError) { console.error('Blocked by Conditional Access Policy'); // Take screenshot for debugging await page.screenshot({ path: 'ca-policy-error.png' }); console.log('Screenshot saved: ca-policy-error.png'); return false; } // Wait for redirect away from Microsoft login try { await page.waitForURL( (url) => !url.href.includes('login.microsoftonline.com'), { timeout } ); return true; } catch { return false; } } // Check if user is already authenticated async function isAuthenticated(page: Page, targetUrl: string): Promise { const currentUrl = page.url(); // If we're on the target URL (not a login page), we're likely authenticated if (currentUrl.startsWith(targetUrl)) { // Check for common login page indicators const isLoginPage = await page.evaluate(() => { const loginIndicators = [ 'input[type="password"]', 'form[action*="login"]', 'form[action*="signin"]', '.login-form', '#login', ]; return loginIndicators.some((sel) => document.querySelector(sel) !== null); }); return !isLoginPage; } return false; } // Main authentication function export async function authenticate(options: AuthOptions): Promise { const browser = await launchBrowser({ headless: options.headless ?? true }); const page = await browser.newPage(); const timeout = options.timeout ?? 30000; try { // Navigate to URL console.log(`Navigating to: ${options.url}`); await page.goto(options.url, { timeout: 60000, waitUntil: 'domcontentloaded' }); // Check if already authenticated if (await isAuthenticated(page, options.url)) { return { success: true, finalUrl: page.url(), authType: 'auto', message: 'Already authenticated (session persisted from profile)', }; } // Get credentials const credentials = options.credentials ? options.credentials : getCredentials(); if (!credentials) { // No credentials - open interactive browser console.log('\nNo credentials provided. Opening browser for manual login...'); console.log('Please complete the login process manually.'); console.log('The session will be saved to your profile.'); // Switch to headed mode for manual login await browser.close(); const interactiveBrowser = await launchBrowser({ headless: false }); const interactivePage = await interactiveBrowser.newPage(); await interactivePage.goto(options.url); await promptUser('\nPress Enter when you have completed login...'); const finalUrl = interactivePage.url(); await interactiveBrowser.close(); return { success: true, finalUrl, authType: 'auto', message: 'Manual login completed - session saved to profile', }; } // Detect auth type if auto let authType = options.authType; if (authType === 'auto') { authType = await detectAuthType(page); console.log(`Detected auth type: ${authType}`); } // Handle authentication based on type let success = false; switch (authType) { case 'msal': success = await handleMsalLogin(page, credentials, timeout); break; case 'form': default: success = await handleFormLogin(page, credentials, timeout); break; } const finalUrl = page.url(); return { success, finalUrl, authType, message: success ? `Authentication successful - session saved to profile` : 'Authentication failed', }; } finally { await browser.close(); } } // Navigate to authenticated page (handles auth if needed) export async function navigateAuthenticated( url: string, options?: { credentials?: { username: string; password: string }; headless?: boolean; } ): Promise<{ page: Page; browser: BrowserContext }> { const { page, browser } = await getPage({ headless: options?.headless ?? true }); await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' }); // Check if we need to authenticate if (!(await isAuthenticated(page, url))) { console.log('Session expired or not authenticated. Attempting login...'); // Get credentials const credentials = options?.credentials ?? getCredentials(); if (!credentials) { throw new Error( 'Authentication required but no credentials provided. ' + 'Set CLOAKBROWSER_USERNAME and CLOAKBROWSER_PASSWORD environment variables. Legacy aliases CAMOUFOX_USERNAME and CAMOUFOX_PASSWORD are also supported.' ); } // Detect and handle auth const authType = await detectAuthType(page); let success = false; if (authType === 'msal') { success = await handleMsalLogin(page, credentials, 30000); } else { success = await handleFormLogin(page, credentials, 30000); } if (!success) { await browser.close(); throw new Error('Authentication failed'); } // Navigate back to original URL if we were redirected if (!page.url().startsWith(url)) { await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' }); } } return { page, browser }; } // CLI entry point async function main() { const args = parseArgs(process.argv.slice(2), { string: ['url', 'type', 'username', 'password'], boolean: ['headless', 'help'], default: { type: 'auto', headless: false, // Default to headed for auth so user can see/interact }, alias: { u: 'url', t: 'type', h: 'help', }, }); if (args.help || !args.url) { console.log(` Web Authentication Handler Usage: npx tsx auth.ts --url [options] Options: -u, --url URL to authenticate (required) -t, --type Auth type: auto, form, or msal (default: auto) --username Username/email (or set CLOAKBROWSER_USERNAME env var) --password Password (or set CLOAKBROWSER_PASSWORD env var) --headless Run in headless mode (default: false for auth) -h, --help Show this help message Auth Types: auto Auto-detect authentication type form Generic username/password form msal Microsoft SSO (login.microsoftonline.com) Environment Variables: CLOAKBROWSER_USERNAME Default username/email for authentication CLOAKBROWSER_PASSWORD Default password for authentication Compatibility Aliases: CAMOUFOX_USERNAME Legacy alias for CLOAKBROWSER_USERNAME CAMOUFOX_PASSWORD Legacy alias for CLOAKBROWSER_PASSWORD Examples: # Interactive login (no credentials, opens browser) npx tsx auth.ts --url "https://example.com/login" # Form login with credentials npx tsx auth.ts --url "https://example.com/login" --type form \\ --username "user@example.com" --password "secret" # Microsoft SSO login CLOAKBROWSER_USERNAME=user@company.com CLOAKBROWSER_PASSWORD=secret \\ npx tsx auth.ts --url "https://internal.company.com" --type msal Notes: - Session is saved to ~/.cloakbrowser-profile/ for persistence - After successful auth, subsequent browses will be authenticated - Use --headless false if you need to handle MFA manually `); process.exit(args.help ? 0 : 1); } const authType = args.type as AuthType; if (!['auto', 'form', 'msal'].includes(authType)) { console.error(`Invalid auth type: ${authType}. Must be auto, form, or msal.`); process.exit(1); } try { const result = await authenticate({ url: args.url, authType, credentials: args.username && args.password ? { username: args.username, password: args.password } : undefined, headless: args.headless, }); console.log(`\nAuthentication result:`); console.log(` Success: ${result.success}`); console.log(` Auth type: ${result.authType}`); console.log(` Final URL: ${result.finalUrl}`); console.log(` Message: ${result.message}`); process.exit(result.success ? 0 : 1); } catch (error) { console.error('Error:', error instanceof Error ? error.message : error); process.exit(1); } } // Run if executed directly const isMainModule = process.argv[1]?.includes('auth.ts'); if (isMainModule) { main(); }