DocumentaciónNavegador y CrawlAgent BrowserAutomatización de MFA de GitHub

Automatización de MFA en GitHub

Introducción

El mayor desafío al automatizar el inicio de sesión en GitHub es la autenticación de dos factores (2FA). Ya sea una aplicación autenticadora (TOTP) o un OTP por correo electrónico, los flujos de automatización tradicionales suelen quedarse atascados en este paso debido a:

  • Incapacidad para recuperar automáticamente los códigos de verificación
  • Incapacidad para sincronizar códigos en tiempo real
  • Incapacidad para ingresar códigos automáticamente mediante automatización
  • Entornos de navegador insuficientemente realistas, que activan las verificaciones de seguridad de GitHub

Este artículo demuestra cómo crear un flujo de trabajo completamente automatizado para la 2FA de GitHub utilizando Navegador Scrapeless + Signal CDP, incluyendo:

  • Caso 1: 2FA de GitHub (generación automática con autenticador / TOTP)
  • Caso 2: 2FA de GitHub (escucha automática de OTP por correo electrónico)

Explicaremos el flujo de trabajo completo para cada caso y mostraremos cómo coordinar el script de inicio de sesión con el sistema de escucha del código de verificación en un entorno automatizado.


Caso 1: 2FA de GitHub (modo OTP con autenticador)

El mecanismo de TOTP (contraseña de un solo uso basada en tiempo) de GitHub es ideal para escenarios de automatización. Utilizando Navegador Scrapeless + Signal CDP, el navegador puede automáticamente:

  • Activar un evento cuando llegue a la página de 2FA
  • Generar el código OTP
  • Rellenar el código automáticamente
  • Completar el inicio de sesión

En comparación con los métodos tradicionales de OTP por correo electrónico o SMS, el TOTP ofrece:

  • Códigos generados localmente sin dependencias externas
  • Generación rápida y estable de códigos
  • No requiere una API adicional
  • Totalmente automatizable sin intervención humana

Escenarios aplicables:

  • Cuentas de GitHub que usan Google Authenticator / Authy / 1Password
  • Rutas de páginas de autenticación en dos pasos (2FA): /sessions/two-factor/app o /sessions/two-factor/webauthn

Vídeo

Caso 1: 2FA de GitHub (modo OTP de la aplicación de autenticación)

Paso 1: Conectarse al Navegador Scrapeless

En este paso, establecemos una conexión WebSocket dúplex completa con el Navegador Scrapeless:

import puppeteer from 'puppeteer-core';
 
const query = new URLSearchParams({
  token: "",
  proxyCountry: "ANY",
  sessionRecording: true,
  sessionTTL: 900,
  sessionName: "Data Scraping",
});
 
const connectionURL = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
const browserWSEndpoint = connectionURL;
 
const browser = await puppeteer.connect({ browserWSEndpoint });
console.log("✅ Connected to Scrapeless Browser");

Ventajas:

  • Chrome real en la nube con fuerte anti-detección
  • No consume recursos locales
  • Proxy automático, persistencia y grabación de sesiones
  • Ejecución confiable de flujos de trabajo automatizados de inicio de sesión a gran escala

Paso 2: Inicializar el Administrador de MFA + Proveedor de TOTP

La autenticación múltiple (MFA) se gestiona mediante la comunicación bidireccional CDP de Signal, que:

  • Envía automáticamente mfa_code_request cuando se detecta una página de 2FA
  • Genera códigos TOTP
  • Devuelve los códigos mediante mfa_code_response
  • Envía eventos del resultado del inicio de sesión
import { authenticator } from 'otplib';
 
const TOTP_SECRETS = {
    'github': 'secret-code',
    'default': 'secret-code'
};
 
authenticator.options = {
    digits: 6,
    step: 30,
    window: 1
};

Paso 3: Crear el Administrador de MFA

Aquí creamos un administrador centralizado de MFA para gestionar la comunicación con CDP de Signal:

  • Activar mfa_code_request
  • Esperar y recibir el código de verificación (mfa_code_response)
  • Devolver el código al proceso de inicio de sesión de GitHub
  • Enviar eventos finales del resultado del inicio de sesión
class MFAManager {
    constructor() {
        this.client = null;
    }
 
    setClient(client) {
        this.client = client;
    }
 
    async sendMFARequest(username, provider = 'github') {
        const requestData = {
            type: 'mfa_required',
            provider,
            username,
            timestamp: new Date().toISOString(),
            service: 'github_2fa'
        };
 
        return await this.client.send('Signal.send', {
            event: 'mfa_code_request',
            data: JSON.stringify(requestData)
        });
    }
 
    async waitForMFACode(timeout = 120000) {
        const result = await this.client.send('Signal.wait', {
            event: 'mfa_code_response',
            timeout
        });
 
        if (result.status === 200 && result.data) {
            return JSON.parse(result.data).code;
        } else if (result.status === 408) {
            throw new Error('Waiting for MFA code timed out');
        } else {
            throw new Error(`Signal wait failed, status: ${result.status}`);
        }
    }
 
    async sendMFAResponse(code, username) {
        const responseData = {
            code,
            username,
            timestamp: new Date().toISOString(),
            status: 'code_provided'
        };
 
        return await this.client.send('Signal.send', {
            event: 'mfa_code_response',
            data: JSON.stringify(responseData)
        });
    }
 
    async sendLoginResult(success, username, url, error = null) {
        const resultData = {
            success,
            username,
            url,
            timestamp: new Date().toISOString(),
            error
        };
 
        await this.client.send('Signal.send', {
            event: 'github_login_result',
            data: JSON.stringify(resultData)
        });
    }
}

Ventajas:

  • Flujo de trabajo de 2FA completamente automatizado (solicitud → espera → recuperación → relleno)
  • Gestión unificada de múltiples métodos de autenticación (TOTP/Correo/Push)
  • Elimina la lógica repetida de 2FA en los scripts

Paso 4: Iniciar el Proveedor de Códigos TOTP (gestiona la escucha y el relleno automático de códigos)

En este paso, lanzamos el oyente automático de códigos TOTP, que:

  • Escucha continuamente eventos de mfa_code_request
  • Genera automáticamente códigos TOTP (usando otplib)
  • Envía automáticamente el código de vuelta mediante Signal (mfa_code_response)
  • Reemplaza por completo la entrada manual de códigos
class TOTPCodeProvider {
    constructor(mfaManager) {
        this.mfaManager = mfaManager;
        this.isListening = false;
        this.secrets = TOTP_SECRETS;
    }
 
    async startListening() {
        if (this.isListening) return;
        this.isListening = true;
        this.listenLoop();
    }
 
    async listenLoop() {
        while (this.isListening) {
            try {
                const result = await this.mfaManager.client.send('Signal.wait', {
                    event: 'mfa_code_request',
                    timeout: 5000
                });
 
                if (result.status === 200 && result.data) {
                    const requestData = JSON.parse(result.data);
                    await this.handleMFARequest(requestData);
                }
            } catch (err) {}
 
            await new Promise(r => setTimeout(r, 100));
        }
    }
 
    async handleMFARequest(requestData) {
        const code = this.generateTOTPCode(requestData.provider);
        await this.mfaManager.sendMFAResponse(code, requestData.username);
        console.log(`✅ TOTP code sent via Signal CDP: ${code}`);
    }
 
    generateTOTPCode(service = 'github') {
        const secret = this.secrets[service] || this.secrets.default;
        return authenticator.generate(secret);
    }
}

Ventajas:

  • Automatiza completamente la autenticación en dos pasos (2FA) sin necesidad de intervención manual
  • No se requiere correo electrónico ni aplicación móvil
  • La generación de TOTP es precisa, estable y escalable
  • Compatible con GitHub, AWS, Azure, Google y cualquier proveedor basado en TOTP

Paso 5: Visite la página de inicio de sesión de GitHub e ingrese las credenciales

Este paso gestiona la primera parte del flujo de inicio de sesión de GitHub:

Abrir la página de inicio de sesión → Ingresar nombre de usuario y contraseña → Continuar al siguiente paso (2FA o inicio de sesión directo)

await page.goto('https://github.com/login', { waitUntil: 'networkidle2' });
 
await page.waitForSelector('#login_field', { timeout: 10000 });
 
await page.type('#login_field', githubCredentials.username);
await page.type('#password', githubCredentials.password);
 
await page.click('input[type="submit"][value="Sign in"]');

Ventajas:

  • El uso del navegador Scrapeless garantiza un entorno de navegador realista y estable
  • La escritura automatizada simula el comportamiento de un usuario real, evitando alertas de seguridad de GitHub
  • Garantiza la navegación a la página correcta de 2FA

Paso 6: Detectar la página de 2FA y enviar automáticamente el TOTP

if (currentUrl.includes('/sessions/two-factor')) {
    console.log('🔐 Detected 2FA required');
 
    await mfaManager.sendMFARequest(githubCredentials.username, 'github');
 
    const mfaCode = await mfaManager.waitForMFACode(120000);
 
    await page.waitForSelector('#app_totp');
    await page.type('#app_totp', mfaCode);
 
    await new Promise(resolve => setTimeout(resolve, 5000));
}

Ventajas:

  • Completa la autenticación en dos pasos (2FA) de GitHub sin intervención humana
  • El TOTP se genera automáticamente, evitando retrasos por códigos enviados por correo
  • La comunicación bidireccional con Signal CDP asegura una entrega precisa del código
  • Estabilidad y fiabilidad a nivel empresarial

Paso 7: Enviar el resultado del inicio de sesión mediante Signal CDP

Una vez completado el inicio de sesión en GitHub y la autenticación 2FA, el resultado final se envía a través de CDP (Signal.send) al flujo de trabajo de automatización. Esto permite que su backend, bot o sistema CI/CD sepa en tiempo real si el inicio de sesión fue exitoso.

const isLoggedIn = !finalUrl.includes('/sessions/two-factor')
                && !finalUrl.includes('/login');
 
await mfaManager.sendLoginResult(isLoggedIn, githubCredentials.username, finalUrl);

Código completo

import puppeteer from 'puppeteer-core';
import {authenticator} from 'otplib';
 
const query = new URLSearchParams({
    token: "api-key",
    proxyCountry: "ANY",
    sessionRecording: true,
    sessionTTL: 900,
    sessionName: "GithubLogin",
});
 
const browserWSEndpoint = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
 
// TOTP secrets
const TOTP_SECRETS = {
    'github': 'secret-code',
    'default': 'secret-code'
};
 
// Configure TOTP
authenticator.options = {digits: 6, step: 30, window: 1};
 
// MFA Manager
class MFAManager {
    constructor() {
        this.client = null;
    }
 
    setClient(client) {
        this.client = client;
    }
 
    async sendMFARequest(username, provider = 'github') {
        return await this.client.send('Signal.send', {
            event: 'mfa_code_request',
            data: JSON.stringify({
                type: 'mfa_required',
                provider,
                username,
                timestamp: new Date().toISOString(),
                service: 'github_2fa'
            })
        });
    }
 
    async waitForMFACode(timeout = 120000) {
        const result = await this.client.send('Signal.wait', {
            event: 'mfa_code_response',
            timeout
        });
 
        if (result.status === 200 && result.data) {
            return JSON.parse(result.data).code;
        } else if (result.status === 408) {
            throw new Error('Timeout waiting for MFA code');
        } else {
            throw new Error(`Signal wait failed, status: ${result.status}`);
        }
    }
 
    async sendMFAResponse(code, username) {
        return await this.client.send('Signal.send', {
            event: 'mfa_code_response',
            data: JSON.stringify({code, username, timestamp: new Date().toISOString(), status: 'code_provided'})
        });
    }
 
    async sendLoginResult(success, username, url, error = null) {
        await this.client.send('Signal.send', {
            event: 'github_login_result',
            data: JSON.stringify({success, username, url, timestamp: new Date().toISOString(), error})
        });
    }
}
 
// TOTP provider
class TOTPCodeProvider {
    constructor(mfaManager) {
        this.mfaManager = mfaManager;
        this.isListening = false;
        this.secrets = TOTP_SECRETS;
    }
 
    async startListening() {
        if (this.isListening) return;
        this.isListening = true;
        this.listenLoop();
    }
 
    async listenLoop() {
        while (this.isListening) {
            try {
                const result = await this.mfaManager.client.send('Signal.wait', {
                    event: 'mfa_code_request',
                    timeout: 5000
                });
 
                if (result.status === 200 && result.data) {
                    const requestData = JSON.parse(result.data);
                    await this.handleMFARequest(requestData);
                }
            } catch (error) {
            }
 
            await new Promise(resolve => setTimeout(resolve, 100));
        }
    }
 
    async handleMFARequest(requestData) {
        try {
            const code = this.generateTOTPCode(requestData.provider);
            if (code) {
                await this.mfaManager.sendMFAResponse(code, requestData.username);
                console.log(`✅ TOTP code sent via Signal CDP: ${code}`);
            } else {
                await this.sendErrorResponse(requestData.username, 'Unable to generate TOTP code');
            }
        } catch (error) {
            await this.sendErrorResponse(requestData.username, error.message);
        }
    }
 
    generateTOTPCode(service = 'github') {
        const secret = this.getSecretForService(service);
        if (!secret) throw new Error(`No TOTP secret found for ${service}`);
 
        const token = authenticator.generate(secret);
        if (!token || token.length !== 6 || isNaN(token)) {
            throw new Error(`Invalid TOTP token generated: ${token}`);
        }
 
        return token;
    }
 
    getSecretForService(service) {
        if (this.secrets[service]) return this.secrets[service];
        const lower = service.toLowerCase();
        if (this.secrets[lower]) return this.secrets[lower];
        if (this.secrets.default) return this.secrets.default;
        return null;
    }
 
    async sendErrorResponse(username, errorMessage) {
        await this.mfaManager.client.send('Signal.send', {
            event: 'mfa_code_error',
            data: JSON.stringify({username, error: errorMessage, timestamp: new Date().toISOString()})
        });
    }
 
    stopListening() {
        this.isListening = false;
    }
}
 
// GitHub credentials
const githubCredentials = {
    username: "***@gmail.com",
    password: "****"
};
 
// Main login flow
async function githubLoginWithAutoMFA() {
    let browser, page, codeProvider;
 
    try {
        console.log("🚀 Starting GitHub login flow...");
        browser = await puppeteer.connect({browserWSEndpoint});
 
        const pages = await browser.pages();
        page = pages.length > 0 ? pages[0] : await browser.newPage();
 
        const client = await page.target().createCDPSession();
        const mfaManager = new MFAManager();
        mfaManager.setClient(client);
 
        codeProvider = new TOTPCodeProvider(mfaManager);
        await codeProvider.startListening();
 
        page.setDefaultTimeout(30000);
        page.setDefaultNavigationTimeout(30000);
 
        await page.goto('https://github.com/login', {waitUntil: 'networkidle2'});
        await page.waitForSelector('#login_field, input[name="login"]', {timeout: 10000});
 
        await page.type('#login_field', githubCredentials.username);
        await page.type('#password', githubCredentials.password);
        await page.click('input[type="submit"][value="Sign in"]');
 
        await new Promise(resolve => setTimeout(resolve, 3000));
 
        const currentUrl = page.url();
 
        if (currentUrl.includes('/sessions/two-factor')) {
            console.log('🔐 2FA required detected');
            await mfaManager.sendMFARequest(githubCredentials.username, 'github');
 
            if (currentUrl.includes('/sessions/two-factor/webauthn')) {
                const moreOptionsButton = await page.$('.more-options-two-factor');
                if (moreOptionsButton) {
                    await moreOptionsButton.click();
                    await new Promise(resolve => setTimeout(resolve, 1000));
                }
 
                const authAppLink = await page.$('a[href="/sessions/two-factor/app"]');
                if (authAppLink) {
                    await authAppLink.click();
                    await page.waitForNavigation({waitUntil: 'networkidle2'});
                }
            }
 
            if (page.url().includes('/sessions/two-factor/app')) {
                const mfaCode = await mfaManager.waitForMFACode(120000);
 
                await page.waitForSelector('#app_totp', {timeout: 10000});
                await page.click('#app_totp', {clickCount: 3});
                await page.type('#app_totp', mfaCode);
 
                await new Promise(resolve => setTimeout(resolve, 5000));
 
                const finalUrl = page.url();
                const isLoggedIn = !finalUrl.includes('/sessions/two-factor') &&
                    !finalUrl.includes('/login') &&
                    (finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
 
                await mfaManager.sendLoginResult(isLoggedIn, githubCredentials.username, finalUrl);
 
                if (isLoggedIn) {
                    console.log('🎉 GitHub login successful!');
                    await page.goto('https://github.com/', {waitUntil: 'networkidle2', timeout: 10000});
                } else {
                    console.log('❌ Login failed');
                }
            }
        } else if (currentUrl.includes('github.com') && !currentUrl.includes('/login')) {
            console.log('✅ Login successful (no 2FA)');
            await mfaManager.sendLoginResult(true, githubCredentials.username, currentUrl);
        } else {
            console.log('❌ Login failed, still on login page');
            await mfaManager.sendLoginResult(false, githubCredentials.username, currentUrl, 'Login failed');
        }
 
    } catch (error) {
        console.error('❌ GitHub login flow failed:', error);
 
        try {
            if (page) {
                const client = await page.target().createCDPSession();
                const mfaManager = new MFAManager();
                mfaManager.setClient(client);
                await mfaManager.sendLoginResult(false, githubCredentials.username, page.url() || 'unknown', error.message);
            }
        } catch (signalError) {
            console.error('❌ Failed to send error signal:', signalError);
        }
 
    } finally {
        if (codeProvider) codeProvider.stopListening();
        if (browser) await browser.close();
        console.log('🔚 GitHub login script finished');
    }
}
 
githubLoginWithAutoMFA().catch(console.error);

Caso 2: GitHub 2FA (modo OTP por correo electrónico)

El tipo más común de autenticación en dos pasos (2FA) en entornos empresariales es la autenticación multifactor (MFA) mediante OTP por correo electrónico.

Aplicable a:

  • GitHub con MFA habilitado y verificación por correo electrónico vinculada
  • Políticas de seguridad de GitHub que requieren verificación por correo electrónico

Vídeo

Caso 2: GitHub 2FA (modo OTP por correo electrónico)

Paso 1: Conectarse al Navegador Scrapeless

import puppeteer from 'puppeteer-core';
 
const query = new URLSearchParams({
  token: "",
  proxyCountry: "ANY",
  sessionRecording: true,
  sessionTTL: 900,
  sessionName: "Data Scraping",
});
 
const connectionURL = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
const browserWSEndpoint = connectionURL;

Ventajas:

  • Ejecución del navegador remoto, sin consumo de recursos locales
  • sessionRecording facilita la reproducción y auditoría
  • Compatible con comunicación bidireccional en tiempo real con Signal

Paso 2: Abrir GitHub e ingresar credenciales

const githubCredentials = {
    username: "1040111453@qq.com",
    password: "shijee1218",
    twoFactorCode: null
};
 
const browser = await puppeteer.connect({ browserWSEndpoint });
const pages = await browser.pages();
const page = pages.length > 0 ? pages[0] : await browser.newPage();
 
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
 
await page.goto('https://github.com/login', { waitUntil: 'networkidle2' });
await page.waitForSelector('#login_field', { timeout: 10000 });

Ventajas:

  • Simula con precisión la entrada de usuario real para evitar activar las medidas de seguridad de GitHub
  • Espera automáticamente a que se cargue la página, mejorando la estabilidad del script
  • Admite configuraciones de tiempo de espera prolongado para manejar fluctuaciones de red

Paso 3: Detectar si se ha cargado la página de 2FA por correo electrónico

const currentUrl = page.url();
 
if (currentUrl.includes('/sessions/verified-device')) {
    const client = await page.target().createCDPSession();
    
    // Send Signal notification to email listener to prepare for OTP
    await client.send('Signal.send', {
        event: 'github_2fa_required',
        data: JSON.stringify({ status: '2fa_required', timestamp: new Date().toISOString() })
    });
 
    // Wait for email listener to return OTP
    const twoFactorResult = await client.send('Signal.wait', {
        event: 'github_2fa_code',
        timeout: 120000
    });

Ventajas:

  • Identifica con precisión la /sessions/verified-device página
  • Notifica proactivamente al oyente de correo electrónico para prepararse para el OTP
  • Admite la espera de un OTP por correo electrónico en tiempo real, mejorando la tasa de éxito de la automatización

Paso 4: El oyente de correo electrónico envía el OTP

Ejemplo de señal:

{
  "event": "github_2fa_code",
  "data": { "code": "123456" }
}

Ventajas:

  • Lee correos electrónicos automáticamente
  • Extrae automáticamente el OTP de GitHub (código de 6 dígitos)
  • Envía la señal al Navegador Scrapeless en tiempo real, sin necesidad de intervención manual

Paso 5: Ingresar el OTP y enviar

if (twoFactorResult.status === 200 && twoFactorResult.data) {
    const twoFactorData = JSON.parse(twoFactorResult.data);
    githubCredentials.twoFactorCode = twoFactorData.code;
 
    if (!page.url().includes('/sessions/verified-device')) return;
 
    await page.$eval('#otp', (input) => { input.value = ''; });
    await page.type('#otp', githubCredentials.twoFactorCode);
 
    await page.evaluate(() => {
        const button = document.querySelector('button[type="submit"]');
        if (button) button.click();
    });
    await new Promise(resolve => setTimeout(resolve, 5000));
}

Ventajas:

  • Rellena automáticamente el OTP, mejorando la eficiencia de la automatización
  • Asegura que la página aún esté en estado de autenticación en dos pasos (2FA), evitando errores de navegación
  • Simula acciones reales de clic, reduciendo el riesgo de activar controles de seguridad

Paso 6: Verificar el resultado final de inicio de sesión y enviar señal

const finalUrl = page.url();
const isLoggedIn =
    !finalUrl.includes('/sessions/verified-device') &&
    !finalUrl.includes('/login') &&
    (finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
 
await client.send('Signal.send', {
    event: 'github_login_result',
    data: JSON.stringify({
        success: isLoggedIn,
        username: githubCredentials.username,
        url: finalUrl,
        twoFactorCode: githubCredentials.twoFactorCode,
        timestamp: new Date().toISOString()
    })
});
 
if (isLoggedIn) await page.goto('https://github.com/', { waitUntil: 'networkidle2' });

Ventajas:

  • Evita juicios erróneos, asegurando que la página haya iniciado sesión correctamente
  • Navega automáticamente a la página principal de GitHub para verificación, mejorando la confiabilidad
  • El resultado del inicio de sesión puede informarse en tiempo real a sistemas CI/CD o bots

Paso 7: Mantener la sesión y cerrar el navegador

await new Promise(resolve => setTimeout(resolve, 5000));
await browser.close();

Ventajas:

  • Asegura que todos los eventos de señal se hayan enviado
  • Mantiene la sesión activa el tiempo suficiente para operaciones posteriores
  • Cierra el navegador para evitar fugas de recursos

Código completo

  1. Primero, debe usar este script para ejecutar la lógica de llenado de nombre de usuario y contraseña en la página de inicio de sesión de GitHub, y esperar la entrada del OTP en la ventana de verificación por correo electrónico.

Obtendrá un taskId cuando se cree la tarea de sesión del navegador; recuérdelo porque lo necesitará en el siguiente paso.

import puppeteer from 'puppeteer-core';
 
const token = "api-key";
 
const query = new URLSearchParams({
    token,
    proxyCountry: "ANY",
    sessionRecording: true,
    sessionTTL: 900,
    sessionName: "Data Scraping",
});
 
const createBrowserSessionURL = `https://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
 
// Get session taskId via HTTP API
const sessionResponse = await fetch(createBrowserSessionURL);
const {taskId} = await sessionResponse.json();
console.log('Session created with task ID:', taskId);
 
const browserWSEndpoint = `wss://api.scrapeless.com/browser/${taskId}?x-api-key=${token}`;
 
async function githubLoginWith2FA() {
    const browser = await puppeteer.connect({browserWSEndpoint});
    let page;
    
    try {
        console.log("🚀 Starting GitHub login process...");
        const githubCredentials = {
            username: "****@gmail.com",
            password: "******",
            twoFactorCode: null
        };
        
        const pages = await browser.pages();
        page = pages.length > 0 ? pages[0] : await browser.newPage();
        
        page.setDefaultTimeout(30000);
        page.setDefaultNavigationTimeout(30000);
        
        console.log('📱 Navigating to GitHub login page...');
        await page.goto('https://github.com/login', {waitUntil: 'networkidle2'});
        
        // Wait for the login form to load
        await page.waitForSelector('#login_field', {timeout: 10000});
        
        console.log('🔑 Typing username and password...');
        await page.type('#login_field', githubCredentials.username);
        await page.type('#password', githubCredentials.password);
        
        // Click the sign in button
        console.log('🖱️ Clicking the sign in button...');
        await page.click('input[type="submit"][value="Sign in"]');
        
        // use setTimeout instead of waitForTimeout
        console.log('⏳ Waiting for page response...');
        await new Promise(resolve => setTimeout(resolve, 3000));
        
        // Check whether an email verification (2FA) is required
        const currentUrl = page.url();
        console.log(`🔍 Current URL: ${currentUrl}`);
        
        if (currentUrl.includes('/sessions/verified-device')) {
            console.log('🔐 Detected email verification required, waiting for verification code...');
            
            const client = await page.target().createCDPSession();
            
            // send signal notifying that email verification code is required
            await client.send('Signal.send', {
                event: 'github_2fa_required',
                data: JSON.stringify({
                    status: '2fa_required',
                    timestamp: new Date().toISOString()
                })
            });
            
            // Wait to receive the email verification code
            console.log('⏳ Waiting for the email verification code...');
            const twoFactorResult = await client.send('Signal.wait', {
                event: 'github_2fa_code',
                timeout: 120000
            });
            
            if (twoFactorResult.status === 200 && twoFactorResult.data) {
                const twoFactorData = JSON.parse(twoFactorResult.data);
                githubCredentials.twoFactorCode = twoFactorData.code;
                
                console.log(`✅ Received email verification code: ${githubCredentials.twoFactorCode}, entering code...`);
                
                // Ensure we are still on the verification page
                if (!page.url().includes('/sessions/verified-device')) {
                    console.log('⚠️ The page has navigated away, verification may no longer be required');
                    return;
                }
                
                // Enter the verification code
                console.log('⌨️ Entering the verification code...');
                await page.$eval('#otp', (input, code) => {
                    input.value = '';
                }, githubCredentials.twoFactorCode);
                
                await page.type('#otp', githubCredentials.twoFactorCode);
                console.log(`✅ Verification code ${githubCredentials.twoFactorCode} has been entered`);
                
                // Click the verify button
                console.log('🖱️ Clicking the verify button...');
                try {
                    await page.evaluate(() => {
                        const button = document.querySelector('button[type="submit"]');
                        if (button) button.click();
                    });
                    
                    console.log('✅ Verify button clicked, waiting for page response...');
                    await new Promise(resolve => setTimeout(resolve, 5000));
                    
                } catch (clickError) {
                    console.log('⚠️ Problem clicking the button:', clickError.message);
                }
                
                // Check login result
                await new Promise(resolve => setTimeout(resolve, 3000));
                const finalUrl = page.url();
                console.log(`🔍 Final URL: ${finalUrl}`);
                
                const isLoggedIn = !finalUrl.includes('/sessions/verified-device') &&
                    !finalUrl.includes('/login') &&
                    (finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
                
                // send login result signal
                if (client) {
                    await client.send('Signal.send', {
                        event: 'github_login_result',
                        data: JSON.stringify({
                            success: isLoggedIn,
                            username: githubCredentials.username,
                            url: finalUrl,
                            twoFactorCode: githubCredentials.twoFactorCode,
                            timestamp: new Date().toISOString()
                        })
                    });
                }
                
                if (isLoggedIn) {
                    console.log('🎉 GitHub login successful!');
                    try {
                        await page.goto('https://github.com/', {
                            waitUntil: 'networkidle2',
                            timeout: 10000
                        });
                        console.log('✅ Successfully accessed GitHub homepage');
                    } catch (profileError) {
                        console.log('⚠️ Problem accessing homepage:', profileError.message);
                    }
                } else {
                    console.log('❌ Email verification failed, login unsuccessful');
                    console.log('🔍 Current page title:', await page.title());
                }
                
            } else {
                console.log('❌ Timed out waiting for the email verification code');
            }
            
        } else if (currentUrl.includes('github.com') && !currentUrl.includes('/login')) {
            // No email verification required
            console.log('✅ Login successful (no email verification required)');
            
            const client = await page.target().createCDPSession();
            await client.send('Signal.send', {
                event: 'github_login_result',
                data: JSON.stringify({
                    success: true,
                    username: githubCredentials.username,
                    url: currentUrl,
                    timestamp: new Date().toISOString()
                })
            });
        } else {
            console.log('❌ Login failed, still on the login page');
            console.log('🔍 Current page title:', await page.title());
        }
        
        // Keep the session for a short time
        console.log('⏳ Keeping connection for 5 seconds...');
        await new Promise(resolve => setTimeout(resolve, 5000));
        
    } catch (error) {
        console.error('❌ GitHub login process failed:', error);
        
        try {
            const pages = await browser.pages();
            const currentPage = pages.length > 0 ? pages[0] : page;
            if (currentPage) {
                const errorClient = await currentPage.target().createCDPSession();
                await errorClient.send('Signal.send', {
                    event: 'github_login_error',
                    data: JSON.stringify({
                        error: error.message,
                        timestamp: new Date().toISOString()
                    })
                });
            }
        } catch (signalError) {
            console.error('❌ Failed to send error signal as well:', signalError);
        }
        
    } finally {
        if (browser) await browser.close();
        console.log('🔚 GitHub login script finished');
    }
}
 
// Run the script
githubLoginWith2FA().catch(console.error);
 
  1. Cuando el script anterior llegue a la página que espera el código de verificación, ejecute inmediatamente este script de oyente de correo electrónico, que enviará el código más reciente al script principal para completar la verificación.
import Imap from 'imap';
import {simpleParser} from 'mailparser';
 
const CONFIG = {
    imap: {
        user: "****@gmail.com",
        password: "****",
        host: "mail.privateemail.com",
        port: 993,
        tls: true,
        tlsOptions: {rejectUnauthorized: false}
    },
 
    signal: {
        baseUrl: "https://browser.scrapeless.com",
        apiKey: "api-key"
    },
 
    checkInterval: 5000,
    maxWaitTime: 120000
};
 
class EmailListener {
    constructor() {
        this.imap = null;
        this.isListening = false;
        this.sessionId = null;
    }
 
    async start(sessionId) {
        console.log('Starting email listener...');
        this.sessionId = sessionId;
 
        try {
            await this.connectIMAP();
            const code = await this.listenForCode();
 
            if (code) {
                console.log(`Found code: ${code}`);
                await this.sendSignal('github_2fa_code', {code});
                console.log('Code sent to browser');
            } else {
                console.log('No code found (timeout)');
                await this.sendSignal('email_listener_timeout', {status: 'timeout'});
            }
        } catch (error) {
            console.error('Listener error:', error.message || error);
        } finally {
            await this.cleanup();
        }
    }
 
    connectIMAP() {
        return new Promise((resolve, reject) => {
            this.imap = new Imap(CONFIG.imap);
 
            this.imap.once('ready', () => {
                console.log('IMAP connected');
                resolve();
            });
 
            this.imap.once('error', reject);
            this.imap.connect();
        });
    }
 
    async listenForCode() {
        console.log('Listening for GitHub verification code...');
        this.isListening = true;
        const startTime = Date.now();
 
        while (this.isListening && (Date.now() - startTime) < CONFIG.maxWaitTime) {
            try {
                const code = await this.checkEmails();
                if (code) return code;
                await new Promise(resolve => setTimeout(resolve, CONFIG.checkInterval));
            } catch (error) {
                console.error('checkEmails failed:', error.message || error);
                await new Promise(resolve => setTimeout(resolve, 10000));
            }
        }
        return null;
    }
 
    checkEmails() {
        return new Promise((resolve, reject) => {
            this.imap.openBox('INBOX', false, (err, box) => {
                if (err) return reject(err);
 
                const criteria = ['UNSEEN', ['FROM', '****@github.com']];
 
                this.imap.search(criteria, (err, results) => {
                    if (err) return reject(err);
                    if (!results || results.length === 0) return resolve(null);
 
                    this.processEmails(results, resolve, reject);
                });
            });
        });
    }
 
    processEmails(results, resolve, reject) {
        const fetcher = this.imap.fetch(results, {
            bodies: ['TEXT'],
            markSeen: true
        });
 
        let processed = 0;
        let foundCode = null;
 
        fetcher.on('message', (msg) => {
            let buffer = '';
 
            msg.on('body', (stream) => {
                stream.on('data', (chunk) => buffer += chunk.toString('utf8'));
            });
 
            msg.once('end', async () => {
                try {
                    const mail = await simpleParser(buffer);
                    const code = this.extractCode(mail.text || '');
                    if (code) foundCode = code;
                } catch (error) {
                    console.error('Failed to parse mail:', error);
                }
 
                processed++;
                if (processed === results.length) resolve(foundCode);
            });
        });
 
        fetcher.once('error', reject);
    }
 
    extractCode(text) {
        const patterns = [
            /verification code:?\s*(\d{6})/i,
            /verification code:?\s*(\d{6})/i,
            /code:?\s*(\d{6})/i,
            /GitHub verification code:?\s*(\d{6})/i
        ];
 
        for (const pattern of patterns) {
            const match = text.match(pattern);
            if (match) return match[1];
        }
 
        const digitMatch = text.match(/\b\d{6}\b/);
        return digitMatch ? digitMatch[0] : null;
    }
 
    // Send signal via HTTP, sessionId as parameter
    async sendSignal(event, data, sessionId = this.sessionId) {
        if (!sessionId) throw new Error('Session ID not available');
 
        try {
            const url = `${CONFIG.signal.baseUrl}/browser/${sessionId}/signal/send`;
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                    'token': CONFIG.signal.apiKey
                },
                body: JSON.stringify({event, data})
            });
 
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
 
            const result = await response.json();
            console.log('Signal sent successfully:', result);
            return result;
        } catch (err) {
            console.error('Failed to send signal via HTTP:', err);
            throw err;
        }
    }
 
    async cleanup() {
        this.isListening = false;
        if (this.imap) {
            try {
                this.imap.end();
                console.log('IMAP connection closed');
            } catch (e) {
                console.error('Error closing IMAP:', e);
            }
        }
        this.sessionId = null;
    }
}
 
const listener = new EmailListener();
listener.start({taskId}).then(); // Replace with taskId from the first step
 

A través de los dos ejemplos de inicio de sesión en GitHub anteriores, demostramos cómo lograr inicios de sesión automatizados eficientes y estables en entornos empresariales, cubriendo los dos modos comunes de autenticación en dos pasos (2FA): TOTP y códigos de verificación por correo electrónico. Usando Scrapeless Browser + Signal CDP, puede realizar operaciones con navegadores reales, simular con precisión el comportamiento del usuario e interactuar en tiempo real con sistemas MFA y oyentes de correo electrónico para recuperar y enviar automáticamente códigos de verificación. Ya sea para desarrollar flujos de trabajo de inicio de sesión automatizados, integrar con sistemas CI/CD o gestionar cuentas empresariales internas, esta solución puede aumentar significativamente las tasas de éxito de inicio de sesión, reducir la intervención manual y proporcionar auditoría y monitoreo operativo completo.