ドキュメントブラウザーと CrawlAgent BrowserGitHub MFA の自動化

GitHub MFA 自動化

はじめに

GitHubログインの自動化における最大の課題は 二要素認証(2FA)。認証アプリ(TOTP)やメールOTPを使用する場合、従来の自動化フローは通常、以下の理由からこの手順で行き詰まります:

  • 検証コードを自動的に取得できないこと
  • コードをリアルタイムに同期できないこと
  • 自動化によってコードを入力できないこと
  • ブラウザ環境が現実的でなく、GitHub のセキュリティチェックをトリガーしてしまうこと

この記事では、 Scrapeless Browser + Signal CDPを使用して完全に自動化されたGitHub 2FAワークフローを構築する方法を紹介します。内容は以下の通りです:

  • ケース1: GitHub 2FA(認証アプリ/TOTPの自動生成)
  • ケース2: GitHub 2FA(メールOTPの自動受信)

各ケースの完全なワークフローを説明し、ログインスクリプトと検証コードリスナーを自動システム内でどのように連携させるかを示します。


ケース 1:GitHub 2FA(認証アプリ OTP モード)

GitHubの TOTP(時間ベースのワンタイムパスワード) 方式は自動化に最適です。 Scrapeless Browser + Signal CDPを使用することで、ブラウザが自動的に以下の処理を行うようになります:

  • 2FA ページに到達したときにイベントをトリガーする
  • OTP コードを生成する
  • コードを自動入力する
  • ログインを完了する

従来のメール/SMS OTP と比較して、TOTP は以下のような利点があります:

  • 外部依存なしでローカルにコードを生成
  • 高速かつ安定したコード生成
  • 追加の API が不要
  • 人的介入なしに完全に自動化可能

適用可能なシナリオ:

  • Google Authenticator/Authy/1Password を使用している GitHub アカウント
  • 2FAページのパス: /sessions/two-factor/app または /sessions/two-factor/webauthn

動画

ケース 1:GitHub 2FA(認証アプリ OTP モード)

ステップ 1:Scrapeless Browser に接続

このステップでは、Scrapeless Browser へ双方向通信可能な WebSocket 接続を確立します:

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");

利点:

  • 強力な検出回避機能を持つクラウド上の本物の Chrome
  • ローカルリソースを消費しない
  • 自動プロキシ、永続化、セッション記録
  • 大規模な自動ログインワークフローを確実に実行

ステップ 2:MFA マネージャー+TOTP プロバイダーの初期化

MFAは Signal CDP双方向通信を通じて処理され、以下の機能を提供します:

  • 2FAページが検出されると自動的に mfa_code_request を送信
  • TOTP コードを生成
  • コードを返すために使用 mfa_code_response
  • ログイン結果のイベントを送信
import { authenticator } from 'otplib';
 
const TOTP_SECRETS = {
    'github': 'secret-code',
    'default': 'secret-code'
};
 
authenticator.options = {
    digits: 6,
    step: 30,
    window: 1
};

ステップ 3:MFA マネージャーの作成

ここでは、Signal CDP との通信を処理するための集中型 MFA マネージャーを作成します:

  • トリガー mfa_code_request
  • 検証コードの受信を待機し、取得(mfa_code_response)
  • コードを GitHub ログインプロセスに戻す
  • 最終的なログイン結果イベントを送信
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)
        });
    }
}

利点:

  • 完全自動化された 2FA ワークフロー(リクエスト → 待機 → 取得 → 入力)
  • 複数の認証方法(TOTP/メール/Push)を統一管理
  • スクリプト内の 2FA 処理の重複を排除

ステップ 4:TOTP コードプロバイダーの起動(コードのリスニングと自動入力を処理)

このステップでは、 TOTP自動コードリスナーを起動します。このリスナーは以下の処理を行います:

  • 継続的に mfa_code_request イベントをリッスン
  • TOTPコードを自動生成( otplibを使用)
  • Signalを介してコードを自動送信(mfa_code_response)
  • 手動でのコード入力を完全に置き換えます
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);
    }
}

利点:

  • 手動入力なしで2FAを完全に自動化します
  • メールやモバイルアプリは必要ありません
  • TOTPの生成は正確で安定しており、スケーラブルです
  • GitHub、AWS、Azure、Google、および任意のTOTPベースのプロバイダーをサポートします

ステップ5:GitHubログインページにアクセスして資格情報を入力

このステップでは、GitHubログインフローの最初の部分を処理します:

ログインページを開く → ユーザー名とパスワードを入力 → 次のステップ(2FAまたは直接ログイン)に進む

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"]');

利点:

  • Scrapeless Browserを使用することで、現実的で安定したブラウザ環境を確保します
  • 自動入力は実際のユーザー操作を模倣し、GitHubのセキュリティフラグを回避します
  • 正しい2FAページへのナビゲーションを保証します

ステップ6:2FAページを検出し、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));
}

利点:

  • 人的介入なしでGitHubの2FAを完了します
  • TOTPが自動生成されるため、メールコードによる遅延が発生しません
  • Signal CDPの双方向通信により、正確なコード配信が保証されます
  • エンタープライズレベルの安定性と信頼性

ステップ7:ログイン結果をSignal CDPで送信

GitHubログイン+2FAが完了した後、最終的なログイン結果はCDP(Signal.send)を通じて自動化ワークフローに送信されます。これにより、バックエンド、ボット、またはCI/CDシステムがリアルタイムでログインが成功したかどうかを把握できます。

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

完全なコード

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);

ケース2:GitHub 2FA(メールOTPモード)

企業環境で最も一般的な2FAの形式は、 メールOTPによる多要素認証(MFA)です。

適用対象:

  • MFAが有効でメール認証が紐づけられているGitHub
  • メール認証を必須とするGitHubのセキュリティポリシー

動画

ケース2:GitHub 2FA(メールOTPモード)

ステップ 1:Scrapeless Browser に接続

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;

利点:

  • リモートブラウザ実行のため、ローカルリソースは消費されません
  • sessionRecording 再生と監査を容易にします
  • Signalとの双方向リアルタイム通信をサポートします

ステップ2:GitHubを開いて資格情報を入力

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 });

利点:

  • GitHubのセキュリティをトリガーしないよう、実際のユーザー入力を正確に模倣します
  • ページのレンダリングを自動的に待機し、スクリプトの安定性を向上させます
  • ネットワークの変動に対応するため、長いタイムアウト設定をサポートします

ステップ3:メール2FAページが読み込まれたかを検出

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
    });

利点:

  • /sessions/verified-device ページを正確に識別
  • OTPの受信準備のために、メールリスナーに事前に通知します
  • リアルタイムのメールOTPを待機する機能をサポートし、自動化の成功率を向上させます

ステップ4:メールリスナーがOTPを送信

Signalの例:

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

利点:

  • 自動的にメールを読み取ります
  • GitHubのOTP(6桁のコード)を自動抽出します
  • リアルタイムでScrapeless BrowserにSignalを送信し、手動操作は不要です

ステップ5:OTPを入力して送信

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));
}

利点:

  • OTPを自動入力し、自動化の効率を向上させます
  • ページがまだ2FA状態にあることを保証し、ナビゲーションエラーを防止します
  • 実際のクリック操作を模倣することで、セキュリティチェックをトリガーするリスクを低減します

ステップ6:最終ログイン結果を確認してSignalを送信

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' });

利点:

  • 誤判定を避け、ページが正常にログインしたことを保証します
  • 信頼性を高めるために、自動的にGitHubのホームページにナビゲートして検証します
  • ログイン結果はCI/CDやボットシステムにリアルタイムで報告できます

ステップ7:セッションを維持してブラウザを閉じる

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

利点:

  • すべてのSignalイベントが送信されたことを保証します
  • 後続の操作に十分な時間、セッションをアクティブに保ちます
  • リソースのリークを防ぐためにブラウザを閉じます

完全なコード

  1. まず、このスクリプトを使用してGitHubログインページのユーザー名とパスワードの入力およびログインロジックを実行し、メール認証ウィンドウでのOTP入力を待機する必要があります。

ブラウザセッションタスクが作成されるとtaskIdが得られます。次のステップで必要になるため、必ず記録してください。

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. 上記のスクリプトが検証コード待ちのページに達した時点で、直ちにこのメールリスナースクリプトを実行してください。これにより、最新のコードがメインスクリプトに送信され、検証が完了します。
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
 

上記の2つのGitHubログインの例を通じて、TOTPおよびメール認証コードという2つの一般的な2FAモードに対応した、エンタープライズ環境における効率的で安定した自動ログインの実現方法を紹介しました。Scrapeless Browser+Signal CDPを使用することで、実際のブラウザ操作を実行し、ユーザー操作を正確に模倣するとともに、MFAシステムやメールリスナーとリアルタイムで相互作用して検証コードを自動的に取得・送信できます。自動ログインワークフローの開発、CI/CDシステムとの統合、社内アカウント管理など、さまざまな用途において、このソリューションはログイン成功率を大幅に向上させ、手動介入を削減し、完全な運用監査とモニタリングを提供します。